This commit is contained in:
启星
2025-08-08 10:49:36 +08:00
parent 6400cf78bb
commit b5ce3d580a
8780 changed files with 978183 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
//
// AlbumPicker.h
// TUIChat
//
// Created by yiliangwang on 2024/10/30.
// Copyright © 2024 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef void (^IAlbumPickerCallback)(NSDictionary *param);
@protocol IAlbumPicker <NSObject>
- (void)pickMediaWithCaller:(UIViewController *)caller
originalMediaPicked:(IAlbumPickerCallback)mediaPicked
progressCallback:(IAlbumPickerCallback)progressCallback
finishedCallback:(IAlbumPickerCallback)finishedCallback;
@end
NS_ASSUME_NONNULL_BEGIN
@interface AlbumPicker : NSObject
@property(nonatomic,strong) id<IAlbumPicker> advancedAlbumPicker;
+ (instancetype)sharedInstance;
+ (void)registerAdvancedAlbumPicker:(id<IAlbumPicker>)albumPicker;
+ (void)pickMediaWithCaller:(UIViewController *)caller
originalMediaPicked:(IAlbumPickerCallback)mediaPicked
progressCallback:(IAlbumPickerCallback)progressCallback
finishedCallback:(IAlbumPickerCallback)finishedCallback;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,46 @@
//
// AlbumPicker.m
// TUIChat
//
// Created by yiliangwang on 2024/10/30.
// Copyright © 2024 Tencent. All rights reserved.
//
#import "AlbumPicker.h"
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface AlbumPicker()
@end
@implementation AlbumPicker
+ (instancetype)sharedInstance {
static AlbumPicker *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
+ (void)registerAdvancedAlbumPicker:(id<IAlbumPicker>)albumPicker {
[AlbumPicker sharedInstance].advancedAlbumPicker = albumPicker;
}
+ (void)pickMediaWithCaller:(UIViewController *)caller
originalMediaPicked:(IAlbumPickerCallback)mediaPicked
progressCallback:(IAlbumPickerCallback)progressCallback
finishedCallback:(IAlbumPickerCallback)finishedCallback {
id<IAlbumPicker> albumPicker = nil;
if ([AlbumPicker sharedInstance].advancedAlbumPicker) {
albumPicker = [AlbumPicker sharedInstance].advancedAlbumPicker;
}
if (albumPicker && [albumPicker respondsToSelector:@selector
(pickMediaWithCaller:originalMediaPicked:progressCallback:finishedCallback:)]) {
[albumPicker pickMediaWithCaller:caller originalMediaPicked:mediaPicked progressCallback:progressCallback finishedCallback:finishedCallback];
}
}
@end

View File

@@ -0,0 +1,42 @@
//
// VideoRecorder.h
// TUIChat
//
// Created by yiliangwang on 2024/10/30.
// Copyright © 2024 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^VideoRecorderSuccessBlock)(NSURL *uri);
typedef void (^VideoRecorderFailureBlock)(NSInteger errorCode, NSString *errorMessage);
@protocol IMultimediaRecorder <NSObject>
- (void)recordVideoWithCaller:(UIViewController *)caller
successBlock:(VideoRecorderSuccessBlock)successBlock
failureBlock:(VideoRecorderFailureBlock)failureBlock;
-(void)takePhoneWithCaller:(UIViewController *)caller
successBlock:(VideoRecorderSuccessBlock)successBlock
failureBlock:(VideoRecorderFailureBlock)failureBlock;
@end
@interface MultimediaRecorder : NSObject
@property (nonatomic, strong) id<IMultimediaRecorder> advancedVideoRecorder;
+ (instancetype)sharedInstance;
+ (void)registerAdvancedVideoRecorder:(id<IMultimediaRecorder>)videoRecorder;
- (void)recordVideoWithCaller:(UIViewController *)caller
successBlock:(VideoRecorderSuccessBlock)successBlock
failureBlock:(VideoRecorderFailureBlock)failureBlock;
-(void)takePhoneWithCaller:(UIViewController *)caller
successBlock:(VideoRecorderSuccessBlock)successBlock
failureBlock:(VideoRecorderFailureBlock)failureBlock;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,44 @@
//
// VideoRecorder.m
// TUIChat
//
// Created by yiliangwang on 2024/10/30.
// Copyright © 2024 Tencent. All rights reserved.
//
#import "MultimediaRecorder.h"
@interface MultimediaRecorder ()
@end
@implementation MultimediaRecorder
+ (instancetype)sharedInstance {
static MultimediaRecorder *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
+ (void)registerAdvancedVideoRecorder:(id<IMultimediaRecorder>)videoRecorder {
[MultimediaRecorder sharedInstance].advancedVideoRecorder = videoRecorder;
}
- (void)recordVideoWithCaller:(UIViewController *)caller
successBlock:(VideoRecorderSuccessBlock)successBlock
failureBlock:(VideoRecorderFailureBlock)failureBlock {
id<IMultimediaRecorder> videoRecorder = nil;
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
videoRecorder = [MultimediaRecorder sharedInstance].advancedVideoRecorder;
}
if (videoRecorder && [videoRecorder respondsToSelector:@selector
(recordVideoWithCaller:successBlock:failureBlock:)]) {
[videoRecorder recordVideoWithCaller:caller successBlock:successBlock failureBlock:failureBlock];
}
}
@end

View File

@@ -0,0 +1,20 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, TUICameraMediaType) {
TUICameraMediaTypePhoto = 1,
TUICameraMediaTypeVideo = 2,
};
typedef NS_ENUM(NSUInteger, TUICameraViewAspectRatio) {
TUICameraViewAspectRatio1x1,
TUICameraViewAspectRatio16x9,
TUICameraViewAspectRatio5x4,
};
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,32 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICameraManager : NSObject
- (AVCaptureDeviceInput *)switchCamera:(AVCaptureSession *)session old:(AVCaptureDeviceInput *)oldinput new:(AVCaptureDeviceInput *)newinput;
- (id)resetFocusAndExposure:(AVCaptureDevice *)device;
- (id)zoom:(AVCaptureDevice *)device factor:(CGFloat)factor;
- (id)focus:(AVCaptureDevice *)device point:(CGPoint)point;
- (id)expose:(AVCaptureDevice *)device point:(CGPoint)point;
- (id)changeFlash:(AVCaptureDevice *)device mode:(AVCaptureFlashMode)mode;
- (id)changeTorch:(AVCaptureDevice *)device model:(AVCaptureTorchMode)mode;
- (AVCaptureFlashMode)flashMode:(AVCaptureDevice *)device;
- (AVCaptureTorchMode)torchMode:(AVCaptureDevice *)device;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,174 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICameraManager.h"
@implementation TUICameraManager
#pragma mark - - Switch Camera
- (AVCaptureDeviceInput *)switchCamera:(AVCaptureSession *)session old:(AVCaptureDeviceInput *)oldinput new:(AVCaptureDeviceInput *)newinput {
[session beginConfiguration];
[session removeInput:oldinput];
if ([session canAddInput:newinput]) {
[session addInput:newinput];
[session commitConfiguration];
return newinput;
} else {
[session addInput:oldinput];
[session commitConfiguration];
return oldinput;
}
}
#pragma mark - - Zoom
- (id)zoom:(AVCaptureDevice *)device factor:(CGFloat)factor {
if (device.activeFormat.videoMaxZoomFactor > factor && factor >= 1.0) {
NSError *error;
if ([device lockForConfiguration:&error]) {
[device rampToVideoZoomFactor:factor withRate:4.0];
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Unsupported zoom factor" code:2000];
}
#pragma mark - - Focus
- (id)focus:(AVCaptureDevice *)device point:(CGPoint)point {
BOOL supported = [device isFocusPointOfInterestSupported] && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus];
if (supported) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.focusPointOfInterest = point;
device.focusMode = AVCaptureFocusModeAutoFocus;
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Device does not support focus" code:2001];
}
#pragma mark - - Expose
static const NSString *CameraAdjustingExposureContext;
- (id)expose:(AVCaptureDevice *)device point:(CGPoint)point {
BOOL supported = [device isExposurePointOfInterestSupported] && [device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure];
if (supported) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.exposurePointOfInterest = point;
device.exposureMode = AVCaptureExposureModeContinuousAutoExposure;
if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
[device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:&CameraAdjustingExposureContext];
}
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Device does not support exposure" code:2002];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == &CameraAdjustingExposureContext) {
AVCaptureDevice *device = (AVCaptureDevice *)object;
if (!device.isAdjustingExposure && [device isExposureModeSupported:AVCaptureExposureModeLocked]) {
[object removeObserver:self forKeyPath:@"adjustingExposure" context:&CameraAdjustingExposureContext];
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error;
if ([device lockForConfiguration:&error]) {
device.exposureMode = AVCaptureExposureModeLocked;
[device unlockForConfiguration];
} else {
NSLog(@"%@", error);
}
});
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - - Auto focus, exposure
- (id)resetFocusAndExposure:(AVCaptureDevice *)device {
AVCaptureFocusMode focusMode = AVCaptureFocusModeContinuousAutoFocus;
AVCaptureExposureMode exposureMode = AVCaptureExposureModeContinuousAutoExposure;
BOOL canResetFocus = [device isFocusPointOfInterestSupported] && [device isFocusModeSupported:focusMode];
BOOL canResetExposure = [device isExposurePointOfInterestSupported] && [device isExposureModeSupported:exposureMode];
CGPoint centerPoint = CGPointMake(0.5f, 0.5f);
NSError *error;
if ([device lockForConfiguration:&error]) {
if (canResetFocus) {
device.focusMode = focusMode;
device.focusPointOfInterest = centerPoint;
}
if (canResetExposure) {
device.exposureMode = exposureMode;
device.exposurePointOfInterest = centerPoint;
}
[device unlockForConfiguration];
}
return error;
}
#pragma mark - - Flash
- (AVCaptureFlashMode)flashMode:(AVCaptureDevice *)device {
return [device flashMode];
}
- (id)changeFlash:(AVCaptureDevice *)device mode:(AVCaptureFlashMode)mode {
if (![device hasFlash]) {
return [self error:@"Flash is not supported" code:2003];
}
if ([self torchMode:device] == AVCaptureTorchModeOn) {
[self setTorch:device model:AVCaptureTorchModeOff];
}
return [self setFlash:device mode:mode];
}
- (id)setFlash:(AVCaptureDevice *)device mode:(AVCaptureFlashMode)mode {
if ([device isFlashModeSupported:mode]) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.flashMode = mode;
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Flash is not supported" code:2003];
}
#pragma mark - - Flashlight
- (AVCaptureTorchMode)torchMode:(AVCaptureDevice *)device {
return [device torchMode];
}
- (id)changeTorch:(AVCaptureDevice *)device model:(AVCaptureTorchMode)mode {
if (![device hasTorch]) {
return [self error:@"Flashlight not supported" code:2004];
}
if ([self flashMode:device] == AVCaptureFlashModeOn) {
[self setFlash:device mode:AVCaptureFlashModeOff];
}
return [self setTorch:device model:mode];
}
- (id)setTorch:(AVCaptureDevice *)device model:(AVCaptureTorchMode)mode {
if ([device isTorchModeSupported:mode]) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.torchMode = mode;
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Flashlight not supported" code:2004];
}
#pragma mark -
- (NSError *)error:(NSString *)text code:(NSInteger)code {
NSDictionary *desc = @{NSLocalizedDescriptionKey : text};
NSError *error = [NSError errorWithDomain:@"com.tui.camera" code:code userInfo:desc];
return error;
}
@end

View File

@@ -0,0 +1,86 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
#import "TUICameraMacro.h"
#import "TUICaptureVideoPreviewView.h"
NS_ASSUME_NONNULL_BEGIN
@class TUICameraView;
@protocol TUICameraViewDelegate <NSObject>
@optional
/**
* Flash
*/
- (void)flashLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
/**
* Fill light
*/
- (void)torchLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
/**
*
* Switch camera
*/
- (void)swicthCameraAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
/**
*
* Auto focus and exposure
*/
- (void)autoFocusAndExposureAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
/**
*
* Foucus
*/
- (void)focusAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *error))handle;
/**
*
* Expose
*/
- (void)exposAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *error))handle;
/**
*
* Zoom
*/
- (void)zoomAction:(TUICameraView *)cameraView factor:(CGFloat)factor;
- (void)cancelAction:(TUICameraView *)cameraView;
- (void)pictureLibAction:(TUICameraView *)cameraView;
- (void)takePhotoAction:(TUICameraView *)cameraView;
- (void)stopRecordVideoAction:(TUICameraView *)cameraView RecordDuration:(CGFloat)duration;
- (void)startRecordVideoAction:(TUICameraView *)cameraView;
- (void)didChangeTypeAction:(TUICameraView *)cameraView type:(TUICameraMediaType)type;
@end
@interface TUICameraView : UIView
@property(nonatomic, weak) id<TUICameraViewDelegate> delegate;
@property(nonatomic, readonly) TUICaptureVideoPreviewView *previewView;
/// default TUICameraMediaTypePhoto
@property(nonatomic) TUICameraMediaType type;
/// default TUICameraViewAspectRatio16x9
@property(nonatomic) TUICameraViewAspectRatio aspectRatio;
/// default 15s
@property(nonatomic, assign) CGFloat maxVideoCaptureTimeLimit;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,434 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICameraView.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import "TUICaptureTimer.h"
static CGFloat gPhotoBtnZoomInRatio = 1.125;
static CGFloat gProgressLayerLineWidth = 5.0;
@interface TUICameraView ()
@property(nonatomic) UIView *contentView;
@property(nonatomic) UIButton *switchCameraButton;
@property(nonatomic) UIButton *closeButton;
@property(nonatomic, strong) UIButton *pictureLibButton;
@property(nonatomic) UIView *focusView;
@property(nonatomic) UISlider *slider;
@property(nonatomic) UIView *photoBtn;
@property(nonatomic) UIView *photoStateView;
@property(nonatomic) UILongPressGestureRecognizer *longPress;
@property(nonatomic) CGRect lastRect;
@property(nonatomic) CAShapeLayer *progressLayer;
@property(nonatomic) TUICaptureTimer *timer;
@property(nonatomic) BOOL isVideoRecording;
@end
@implementation TUICameraView
@synthesize previewView = _previewView;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.type = TUICameraMediaTypePhoto;
self.aspectRatio = TUICameraViewAspectRatio16x9;
self.backgroundColor = [UIColor blackColor];
self.maxVideoCaptureTimeLimit = 15.0;
}
return self;
}
- (void)setupUI {
[self addSubview:self.contentView];
[self.contentView addSubview:self.previewView];
[self.contentView addSubview:self.switchCameraButton];
[self.contentView addSubview:self.photoBtn];
[self.contentView addSubview:self.closeButton];
[self.contentView addSubview:self.pictureLibButton];
[self.previewView addSubview:self.focusView];
[self.previewView addSubview:self.slider];
self.timer = ({
TUICaptureTimer *timer = [TUICaptureTimer new];
timer.maxCaptureTime = self.maxVideoCaptureTimeLimit;
__weak __typeof(self) weakSelf = self;
timer.progressBlock = ^(CGFloat ratio, CGFloat recordTime) {
weakSelf.progress = ratio;
};
timer.progressFinishBlock = ^(CGFloat ratio, CGFloat recordTime) {
weakSelf.progress = 1;
self.longPress.enabled = NO;
[weakSelf endVideoRecordWithCaptureDuration:recordTime];
self.longPress.enabled = YES;
};
timer.progressCancelBlock = ^{
weakSelf.progress = 0;
};
timer;
});
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
[self.previewView addGestureRecognizer:tap];
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchAction:)];
[self.previewView addGestureRecognizer:pinch];
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGRectEqualToRect(self.lastRect, self.bounds)) {
[self setupUI];
self.lastRect = self.bounds;
self.contentView.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
CGFloat previewViewWidth = self.contentView.bounds.size.width;
CGFloat previewViewHeight;
switch (self.aspectRatio) {
case TUICameraViewAspectRatio1x1:
previewViewHeight = previewViewWidth;
break;
case TUICameraViewAspectRatio16x9:
previewViewHeight = previewViewWidth * (16.0 / 9.0);
break;
case TUICameraViewAspectRatio5x4:
previewViewHeight = previewViewWidth * (5.0 / 4.0);
break;
default:
break;
}
CGFloat previewViewY = (self.contentView.bounds.size.height - previewViewHeight) / 2.0;
self.previewView.frame = CGRectMake(0, previewViewY, self.contentView.bounds.size.width, previewViewHeight);
CGFloat switchCameraButtonWidth = 44.0;
self.switchCameraButton.frame =
CGRectMake(self.contentView.bounds.size.width - switchCameraButtonWidth - 16.0, 30.0, switchCameraButtonWidth, switchCameraButtonWidth);
if (isRTL()) {
[self.switchCameraButton resetFrameToFitRTL];
}
CGFloat photoBtnWidth = 100.0;
self.photoBtn.frame = CGRectMake((self.contentView.bounds.size.width - photoBtnWidth) / 2.0, self.contentView.bounds.size.height - photoBtnWidth - 30,
photoBtnWidth, photoBtnWidth);
self.photoBtn.layer.cornerRadius = photoBtnWidth / 2.0;
CGFloat distanceToPhotoBtn = 10.0;
CGFloat photoStateViewWidth = photoBtnWidth - 2 * distanceToPhotoBtn;
self.photoStateView.frame = CGRectMake(distanceToPhotoBtn, distanceToPhotoBtn, photoStateViewWidth, photoStateViewWidth);
self.photoStateView.layer.cornerRadius = photoStateViewWidth / 2.0;
if (self.type == TUICameraMediaTypeVideo) {
self.progressLayer.frame = CGRectInset(self.photoBtn.bounds, gProgressLayerLineWidth / 2.0, gProgressLayerLineWidth / 2.0);
CGFloat radius = self.progressLayer.bounds.size.width / 2;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
radius:radius
startAngle:-M_PI_2
endAngle:-M_PI_2 + M_PI * 2
clockwise:YES];
self.progressLayer.path = path.CGPath;
[self.photoBtn.layer addSublayer:self.progressLayer];
}
CGFloat closeButtonWidth = 30.0;
CGFloat closeButtonX = (self.photoBtn.frame.origin.x - closeButtonWidth) / 2.0;
CGFloat closeButtonY = self.photoBtn.center.y - closeButtonWidth / 2.0;
self.closeButton.frame = CGRectMake(closeButtonX, closeButtonY, closeButtonWidth, closeButtonWidth);
if (isRTL()) {
[self.closeButton resetFrameToFitRTL];
}
CGFloat pictureButtonWidth = 30.0;
self.pictureLibButton.frame = CGRectMake(self.contentView.frame.size.width - closeButtonX, closeButtonY, pictureButtonWidth, pictureButtonWidth);
if (isRTL()) {
[self.pictureLibButton resetFrameToFitRTL];
}
self.slider.transform = CGAffineTransformMakeRotation(M_PI_2);
self.slider.frame = CGRectMake(self.bounds.size.width - 50, 50, 15, 200);
}
}
#pragma mark -
- (void)setProgress:(CGFloat)progress {
if (progress < 0) {
return;
} else if (progress < 1.0) {
self.progressLayer.strokeEnd = progress;
}
if (progress >= 1.0) {
self.progressLayer.strokeEnd = 1.0;
}
}
#pragma mark - Event Response
- (void)tapAction:(UIGestureRecognizer *)tap {
if ([_delegate respondsToSelector:@selector(focusAction:point:handle:)]) {
CGPoint point = [tap locationInView:self.previewView];
[self runFocusAnimation:self.focusView point:point];
[_delegate focusAction:self
point:[self.previewView captureDevicePointForPoint:point]
handle:^(NSError *error) {
if (error) NSAssert1(NO, @"%@", error); //[self showError:error];
}];
}
}
- (void)runFocusAnimation:(UIView *)view point:(CGPoint)point {
view.center = point;
view.hidden = NO;
[UIView animateWithDuration:0.15f
delay:0.0f
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
view.layer.transform = CATransform3DMakeScale(0.5, 0.5, 1.0);
}
completion:^(BOOL complete) {
double delayInSeconds = 0.5f;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
view.hidden = YES;
view.transform = CGAffineTransformIdentity;
});
}];
}
- (void)pinchAction:(UIPinchGestureRecognizer *)pinch {
if ([_delegate respondsToSelector:@selector(zoomAction:factor:)]) {
if (pinch.state == UIGestureRecognizerStateBegan) {
[UIView animateWithDuration:0.1
animations:^{
self->_slider.alpha = 1;
}];
} else if (pinch.state == UIGestureRecognizerStateChanged) {
if (pinch.velocity > 0) {
_slider.value += pinch.velocity / 100;
} else {
_slider.value += pinch.velocity / 20;
}
[_delegate zoomAction:self factor:powf(5, _slider.value)];
} else {
[UIView animateWithDuration:0.1
animations:^{
self->_slider.alpha = 0.0;
}];
}
}
}
- (void)switchCameraButtonClick:(UIButton *)btn {
if ([self.delegate respondsToSelector:@selector(swicthCameraAction:handle:)]) {
[self.delegate swicthCameraAction:self
handle:^(NSError *_Nonnull error){
//
}];
}
}
- (void)closeButtonClick:(UIButton *)btn {
if ([self.delegate respondsToSelector:@selector(cancelAction:)]) {
[self.delegate cancelAction:self];
}
}
- (void)pictureLibClick:(UIButton *)btn {
if ([self.delegate respondsToSelector:@selector(pictureLibAction:)]) {
[self.delegate pictureLibAction:self];
}
}
- (void)longPressGesture:(UILongPressGestureRecognizer *)gesture {
switch (gesture.state) {
case UIGestureRecognizerStateBegan: {
[self beginVideoRecord];
break;
}
case UIGestureRecognizerStateChanged: {
break;
}
case UIGestureRecognizerStateEnded: {
[self endVideoRecordWithCaptureDuration:self.timer.captureDuration];
break;
}
default:
break;
}
}
- (void)beginVideoRecord {
if (self.isVideoRecording) {
return;
}
self.closeButton.hidden = YES;
self.isVideoRecording = YES;
self.pictureLibButton.hidden = YES;
[self.timer startTimer];
dispatch_async(dispatch_get_main_queue(), ^{
self.progressLayer.strokeEnd = 0.0;
[UIView animateWithDuration:0.2
animations:^{
self.photoStateView.transform = CGAffineTransformMakeScale(.5, .5);
self.photoBtn.transform = CGAffineTransformMakeScale(gPhotoBtnZoomInRatio, gPhotoBtnZoomInRatio);
}];
if ([self.delegate respondsToSelector:@selector(startRecordVideoAction:)]) {
[self.delegate startRecordVideoAction:self];
}
});
}
- (void)endVideoRecordWithCaptureDuration:(CGFloat)duration {
if (self.isVideoRecording == NO) {
return;
}
self.closeButton.hidden = NO;
self.isVideoRecording = NO;
self.pictureLibButton.hidden = NO;
[self.timer stopTimer];
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.2
animations:^{
self.photoStateView.transform = CGAffineTransformIdentity;
self.photoBtn.transform = CGAffineTransformIdentity;
}];
if ([self.delegate respondsToSelector:@selector(stopRecordVideoAction:RecordDuration:)]) {
[self.delegate stopRecordVideoAction:self RecordDuration:duration];
}
self.progressLayer.strokeEnd = 0.0;
});
}
- (void)tapGesture:(UITapGestureRecognizer *)tapGesture {
if ([_delegate respondsToSelector:@selector(takePhotoAction:)]) {
[_delegate takePhotoAction:self];
}
}
#pragma mark - Getters & Setters
- (UIView *)contentView {
if (!_contentView) {
_contentView = [UIView new];
}
return _contentView;
}
- (TUICaptureVideoPreviewView *)previewView {
if (!_previewView) {
_previewView = [[TUICaptureVideoPreviewView alloc] init];
_previewView.userInteractionEnabled = YES;
}
return _previewView;
}
- (UIButton *)switchCameraButton {
if (!_switchCameraButton) {
_switchCameraButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *switchCameraButtonImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_switchCamera")];
[_switchCameraButton setImage:switchCameraButtonImage forState:UIControlStateNormal];
[_switchCameraButton addTarget:self action:@selector(switchCameraButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _switchCameraButton;
}
- (UIButton *)closeButton {
if (!_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *closeButtonImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_back")];
[_closeButton setBackgroundImage:[closeButtonImage rtl_imageFlippedForRightToLeftLayoutDirection] forState:UIControlStateNormal];
[_closeButton addTarget:self action:@selector(closeButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _closeButton;
}
- (UIButton *)pictureLibButton {
if (!_pictureLibButton) {
_pictureLibButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *pictureImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"more_picture")];
[_pictureLibButton setBackgroundImage:pictureImage forState:UIControlStateNormal];
[_pictureLibButton addTarget:self action:@selector(pictureLibClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _pictureLibButton;
}
- (UIView *)photoBtn {
if (!_photoBtn) {
_photoBtn = [UIView new];
[_photoBtn setBackgroundColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5]];
if (self.type == TUICameraMediaTypeVideo) {
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGesture:)];
[_photoBtn addGestureRecognizer:longPress];
_longPress = longPress;
}
if (self.type == TUICameraMediaTypePhoto) {
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
[_photoBtn addGestureRecognizer:tapGesture];
}
_photoBtn.userInteractionEnabled = YES;
_photoStateView = [UIView new];
_photoStateView.backgroundColor = [UIColor whiteColor];
[_photoBtn addSubview:_photoStateView];
}
return _photoBtn;
}
- (CAShapeLayer *)progressLayer {
if (!_progressLayer) {
_progressLayer = [CAShapeLayer layer];
_progressLayer.fillColor = [UIColor clearColor].CGColor;
_progressLayer.lineWidth = gProgressLayerLineWidth;
_progressLayer.strokeColor = [UIColor colorWithRed:0 green:204.0 / 255 blue:0 alpha:1].CGColor;
_progressLayer.strokeStart = 0;
_progressLayer.strokeEnd = 0;
_progressLayer.lineCap = kCALineCapButt;
}
return _progressLayer;
}
- (UIView *)focusView {
if (_focusView == nil) {
_focusView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 150, 150.0f)];
_focusView.backgroundColor = [UIColor clearColor];
_focusView.layer.borderColor = [UIColor colorWithRed:0 green:204.0 / 255 blue:0 alpha:1].CGColor;
_focusView.layer.borderWidth = 3.0f;
_focusView.hidden = YES;
}
return _focusView;
}
- (UISlider *)slider {
if (_slider == nil) {
_slider = [[UISlider alloc] init];
_slider.minimumValue = 0;
_slider.maximumValue = 1;
_slider.maximumTrackTintColor = [UIColor whiteColor];
_slider.minimumTrackTintColor = [UIColor whiteColor];
_slider.alpha = 0.0;
_slider.hidden = YES;
}
return _slider;
}
@end

View File

@@ -0,0 +1,32 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
@import UIKit;
#import "TUICameraMacro.h"
@class TUICameraViewController;
@protocol TUICameraViewControllerDelegate <NSObject>
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithVideoURL:(NSURL *)url;
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithImageData:(NSData *)data;
- (void)cameraViewControllerDidCancel:(TUICameraViewController *)controller;
- (void)cameraViewControllerDidPictureLib:(TUICameraViewController *)controller finishCallback:(void (^)(void))callback;
@end
@interface TUICameraViewController : UIViewController
@property(nonatomic, weak) id<TUICameraViewControllerDelegate> delegate;
/// default TUICameraMediaTypePhoto
@property(nonatomic) TUICameraMediaType type;
/// default TUICameraViewAspectRatio16x9
@property(nonatomic) TUICameraViewAspectRatio aspectRatio;
/// default 15s
@property(nonatomic) NSTimeInterval videoMaximumDuration;
/// default 3s
@property(nonatomic) NSTimeInterval videoMinimumDuration;
@end

View File

@@ -0,0 +1,413 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICameraViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import <CoreMedia/CMMetadata.h>
#import <Photos/Photos.h>
#import "TUICameraView.h"
#import "TUICaptureImagePreviewController.h"
#import "TUICaptureVideoPreviewViewController.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import "TUICameraManager.h"
#import "TUICaptureTimer.h"
#import "TUIMotionManager.h"
#import "TUIMovieManager.h"
@interface TUICameraViewController () <AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate, TUICameraViewDelegate> {
AVCaptureSession *_session;
AVCaptureDeviceInput *_deviceInput;
AVCaptureConnection *_videoConnection;
AVCaptureConnection *_audioConnection;
AVCaptureVideoDataOutput *_videoOutput;
AVCaptureStillImageOutput *_imageOutput;
BOOL _recording;
}
@property(nonatomic, strong) TUICameraView *cameraView;
@property(nonatomic, strong) TUIMovieManager *movieManager;
@property(nonatomic, strong) TUICameraManager *cameraManager;
@property(nonatomic, strong) TUIMotionManager *motionManager;
@property(nonatomic, strong) AVCaptureDevice *activeCamera;
@property(nonatomic, strong) AVCaptureDevice *inactiveCamera;
@property(nonatomic) BOOL isFirstShow;
@property(nonatomic) BOOL lastPageBarHidden;
@end
@implementation TUICameraViewController
- (instancetype)init {
self = [super init];
if (self) {
_motionManager = [[TUIMotionManager alloc] init];
_cameraManager = [[TUICameraManager alloc] init];
_type = TUICameraMediaTypePhoto;
_aspectRatio = TUICameraViewAspectRatio16x9;
_videoMaximumDuration = 15.0;
_videoMinimumDuration = 3.0;
_isFirstShow = YES;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.cameraView = [[TUICameraView alloc] initWithFrame:self.view.bounds];
self.cameraView.type = self.type;
self.cameraView.aspectRatio = self.aspectRatio;
self.cameraView.delegate = self;
self.cameraView.maxVideoCaptureTimeLimit = self.videoMaximumDuration;
[self.view addSubview:self.cameraView];
NSError *error;
[self setupSession:&error];
if (!error) {
[self.cameraView.previewView setCaptureSessionsion:_session];
[self startCaptureSession];
} else {
// NSAssert1(NO, @"Camera Initialize Failed : %@", error.localizedDescription);
// [self showErrorStr:error.localizedDescription];
}
}
- (void)dealloc {
[self stopCaptureSession];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.isFirstShow) {
self.isFirstShow = NO;
self.lastPageBarHidden = self.navigationController.navigationBarHidden;
}
self.navigationController.navigationBarHidden = YES;
}
- (void)willMoveToParentViewController:(UIViewController *)parent {
[super willMoveToParentViewController:parent];
if (!parent) {
self.navigationController.navigationBarHidden = self.lastPageBarHidden;
}
}
#pragma mark - - Input Device
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if (device.position == position) {
return device;
}
}
return nil;
}
- (AVCaptureDevice *)activeCamera {
return _deviceInput.device;
}
- (AVCaptureDevice *)inactiveCamera {
AVCaptureDevice *device = nil;
if ([[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count] > 1) {
if ([self activeCamera].position == AVCaptureDevicePositionBack) {
device = [self cameraWithPosition:AVCaptureDevicePositionFront];
} else {
device = [self cameraWithPosition:AVCaptureDevicePositionBack];
}
}
return device;
}
#pragma mark - - Configuration
- (void)setupSession:(NSError **)error {
_session = [[AVCaptureSession alloc] init];
_session.sessionPreset = AVCaptureSessionPresetHigh;
[self setupSessionInputs:error];
[self setupSessionOutputs:error];
}
- (void)setupSessionInputs:(NSError **)error {
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
if (videoInput) {
if ([_session canAddInput:videoInput]) {
[_session addInput:videoInput];
}
}
_deviceInput = videoInput;
if (_type == TUICameraMediaTypeVideo) {
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioIn = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:error];
if ([_session canAddInput:audioIn]) {
[_session addInput:audioIn];
}
}
}
- (void)setupSessionOutputs:(NSError **)error {
dispatch_queue_t captureQueue = dispatch_queue_create("com.tui.captureQueue", DISPATCH_QUEUE_SERIAL);
AVCaptureVideoDataOutput *videoOut = [[AVCaptureVideoDataOutput alloc] init];
[videoOut setAlwaysDiscardsLateVideoFrames:YES];
[videoOut setVideoSettings:@{(id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_32BGRA]}];
[videoOut setSampleBufferDelegate:self queue:captureQueue];
if ([_session canAddOutput:videoOut]) {
[_session addOutput:videoOut];
}
_videoOutput = videoOut;
_videoConnection = [videoOut connectionWithMediaType:AVMediaTypeVideo];
if (_type == TUICameraMediaTypeVideo) {
AVCaptureAudioDataOutput *audioOut = [[AVCaptureAudioDataOutput alloc] init];
[audioOut setSampleBufferDelegate:self queue:captureQueue];
if ([_session canAddOutput:audioOut]) {
[_session addOutput:audioOut];
}
_audioConnection = [audioOut connectionWithMediaType:AVMediaTypeAudio];
}
AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init];
imageOutput.outputSettings = @{AVVideoCodecKey : AVVideoCodecJPEG};
if ([_session canAddOutput:imageOutput]) {
[_session addOutput:imageOutput];
}
_imageOutput = imageOutput;
}
#pragma mark - - Session Control
- (void)startCaptureSession {
if (!_session.isRunning) {
[_session startRunning];
}
}
- (void)stopCaptureSession {
if (_session.isRunning) {
[_session stopRunning];
}
}
#pragma mark - - Camera Operation
- (void)zoomAction:(TUICameraView *)cameraView factor:(CGFloat)factor {
NSError *error = [_cameraManager zoom:[self activeCamera] factor:factor];
if (error) NSLog(@"%@", error);
}
- (void)focusAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *))handle {
NSError *error = [_cameraManager focus:[self activeCamera] point:point];
handle(error);
NSLog(@"%f", [self activeCamera].activeFormat.videoMaxZoomFactor);
}
- (void)exposAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *))handle {
NSError *error = [_cameraManager expose:[self activeCamera] point:point];
handle(error);
}
- (void)autoFocusAndExposureAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
NSError *error = [_cameraManager resetFocusAndExposure:[self activeCamera]];
handle(error);
}
- (void)flashLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
BOOL on = [_cameraManager flashMode:[self activeCamera]] == AVCaptureFlashModeOn;
AVCaptureFlashMode mode = on ? AVCaptureFlashModeOff : AVCaptureFlashModeOn;
NSError *error = [_cameraManager changeFlash:[self activeCamera] mode:mode];
handle(error);
}
- (void)torchLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
BOOL on = [_cameraManager torchMode:[self activeCamera]] == AVCaptureTorchModeOn;
AVCaptureTorchMode mode = on ? AVCaptureTorchModeOff : AVCaptureTorchModeOn;
NSError *error = [_cameraManager changeTorch:[self activeCamera] model:mode];
handle(error);
}
- (void)swicthCameraAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
NSError *error;
AVCaptureDevice *videoDevice = [self inactiveCamera];
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
if (videoInput) {
CATransition *animation = [CATransition animation];
animation.type = @"oglFlip";
animation.subtype = kCATransitionFromLeft;
animation.duration = 0.5;
[self.cameraView.previewView.layer addAnimation:animation forKey:@"flip"];
AVCaptureFlashMode mode = [_cameraManager flashMode:[self activeCamera]];
_deviceInput = [_cameraManager switchCamera:_session old:_deviceInput new:videoInput];
_videoConnection = [_videoOutput connectionWithMediaType:AVMediaTypeVideo];
[_cameraManager changeFlash:[self activeCamera] mode:mode];
}
handle(error);
}
#pragma mark - - Taking Photo
- (void)takePhotoAction:(TUICameraView *)cameraView {
AVCaptureConnection *connection = [_imageOutput connectionWithMediaType:AVMediaTypeVideo];
if (connection.isVideoOrientationSupported) {
connection.videoOrientation = [self currentVideoOrientation];
}
[_imageOutput captureStillImageAsynchronouslyFromConnection:connection
completionHandler:^(CMSampleBufferRef _Nullable imageDataSampleBuffer, NSError *_Nullable error) {
if (error) {
[self showErrorStr:error.localizedDescription];
return;
}
NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
UIImage *image = [[UIImage alloc] initWithData:imageData];
TUICaptureImagePreviewController *vc = [[TUICaptureImagePreviewController alloc] initWithImage:image];
[self.navigationController pushViewController:vc animated:YES];
__weak __typeof(self) weakSelf = self;
vc.commitBlock = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
UIGraphicsBeginImageContext(CGSizeMake(image.size.width, image.size.height));
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
UIImage *convertToUpImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSData *data = UIImageJPEGRepresentation(convertToUpImage, 0.75);
[strongSelf.delegate cameraViewController:strongSelf didFinishPickingMediaWithImageData:data];
[strongSelf popViewControllerAnimated:YES];
};
vc.cancelBlock = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.navigationController popViewControllerAnimated:YES];
};
}];
}
- (void)cancelAction:(TUICameraView *)cameraView {
[self.delegate cameraViewControllerDidCancel:self];
[self popViewControllerAnimated:YES];
}
- (void)pictureLibAction:(TUICameraView *)cameraView {
@weakify(self);
[self.delegate cameraViewControllerDidPictureLib:self
finishCallback:^{
@strongify(self);
[self popViewControllerAnimated:NO];
}];
}
#pragma mark - - Record
- (void)startRecordVideoAction:(TUICameraView *)cameraView {
/**
* Recreate each time to avoid Crash caused by unreleased previous information
*/
_movieManager = [[TUIMovieManager alloc] init];
_recording = YES;
_movieManager.currentDevice = [self activeCamera];
_movieManager.currentOrientation = [self currentVideoOrientation];
@weakify(self);
[_movieManager start:^(NSError *_Nonnull error) {
@strongify(self);
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
if (error) [self showErrorStr:error.localizedDescription];
});
}];
}
- (void)stopRecordVideoAction:(TUICameraView *)cameraView RecordDuration:(CGFloat)duration {
_recording = NO;
@weakify(self);
[_movieManager stop:^(NSURL *_Nonnull url, NSError *_Nonnull error) {
@strongify(self);
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
if (duration < self.videoMinimumDuration) {
[self showErrorStr:TIMCommonLocalizableString(TUIKitMoreVideoCaptureDurationTip)];
} else if (error) {
[self showErrorStr:error.localizedDescription];
} else {
TUICaptureVideoPreviewViewController *videoPreviewController = [[TUICaptureVideoPreviewViewController alloc] initWithVideoURL:url];
[self.navigationController pushViewController:videoPreviewController animated:YES];
@weakify(self);
videoPreviewController.commitBlock = ^{
@strongify(self);
[self.delegate cameraViewController:self didFinishPickingMediaWithVideoURL:url];
[self popViewControllerAnimated:YES];
};
videoPreviewController.cancelBlock = ^{
@strongify(self);
[self.navigationController popViewControllerAnimated:YES];
};
}
});
}];
}
#pragma mark - - AVCaptureVideoDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
if (_recording) {
[_movieManager writeData:connection video:_videoConnection audio:_audioConnection buffer:sampleBuffer];
}
}
#pragma mark - - Others
- (AVCaptureVideoOrientation)currentVideoOrientation {
AVCaptureVideoOrientation orientation;
switch (self.motionManager.deviceOrientation) {
case UIDeviceOrientationPortrait:
orientation = AVCaptureVideoOrientationPortrait;
break;
case UIDeviceOrientationLandscapeLeft:
orientation = AVCaptureVideoOrientationLandscapeRight;
break;
case UIDeviceOrientationLandscapeRight:
orientation = AVCaptureVideoOrientationLandscapeLeft;
break;
case UIDeviceOrientationPortraitUpsideDown:
orientation = AVCaptureVideoOrientationPortraitUpsideDown;
break;
default:
orientation = AVCaptureVideoOrientationPortrait;
break;
}
return orientation;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)popViewControllerAnimated:(BOOL)animated {
NSUInteger index = [self.navigationController.viewControllers indexOfObject:self];
index--;
UIViewController *lastVC = nil;
if (index > 0 && index < self.navigationController.viewControllers.count) {
lastVC = self.navigationController.viewControllers[index];
}
self.navigationController.navigationBarHidden = self.lastPageBarHidden;
if (lastVC) {
[self.navigationController popToViewController:lastVC animated:animated];
} else {
[self.navigationController popViewControllerAnimated:animated];
}
}
- (void)showErrorStr:(NSString *)errStr {
[TUITool makeToast:errStr duration:1 position:CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0)];
}
@end

View File

@@ -0,0 +1,18 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
@import UIKit;
@interface TUICaptureImagePreviewController : UIViewController
@property(nonatomic) void (^commitBlock)(void);
@property(nonatomic) void (^cancelBlock)(void);
- (instancetype)initWithImage:(UIImage *)image NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
@end

View File

@@ -0,0 +1,91 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICaptureImagePreviewController.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
@interface TUICaptureImagePreviewController () {
UIImage *_image;
}
@property(nonatomic) UIImageView *imageView;
@property(nonatomic) UIButton *commitButton;
@property(nonatomic) UIButton *cancelButton;
@property(nonatomic) CGRect lastRect;
@end
@implementation TUICaptureImagePreviewController
- (instancetype)initWithImage:(UIImage *)image {
if (self = [super initWithNibName:nil bundle:nil]) {
_image = image;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
UIImageView *imageView = [[UIImageView alloc] initWithImage:_image];
imageView.layer.masksToBounds = YES;
imageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubview:imageView];
self.imageView = imageView;
NSLog(@"%ld--%ld", (long)_image.imageOrientation, UIImageOrientationUp);
self.commitButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *commitImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commit")];
[self.commitButton setImage:commitImage forState:UIControlStateNormal];
UIImage *commitBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commitBg")];
[self.commitButton setBackgroundImage:commitBGImage forState:UIControlStateNormal];
[self.commitButton addTarget:self action:@selector(commitButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.commitButton];
self.cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *cancelButtonBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_cancel")];
[self.cancelButton setBackgroundImage:cancelButtonBGImage forState:UIControlStateNormal];
[self.cancelButton addTarget:self action:@selector(cancelButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.cancelButton];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (!CGRectEqualToRect(self.lastRect, self.view.bounds)) {
self.lastRect = self.view.bounds;
self.imageView.frame = self.view.bounds;
CGFloat commitButtonWidth = 80.0;
CGFloat buttonDistance = (self.view.bounds.size.width - 2 * commitButtonWidth) / 3.0;
CGFloat commitButtonY = self.view.bounds.size.height - commitButtonWidth - 50.0;
CGFloat commitButtonX = 2 * buttonDistance + commitButtonWidth;
self.commitButton.frame = CGRectMake(commitButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
CGFloat cancelButtonX = commitButtonWidth;
self.cancelButton.frame = CGRectMake(cancelButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
if (isRTL()) {
[self.commitButton resetFrameToFitRTL];
[self.cancelButton resetFrameToFitRTL];
}
}
}
- (void)commitButtonClick:(UIButton *)btn {
if (self.commitBlock) {
self.commitBlock();
}
}
- (void)cancelButtonClick:(UIButton *)btn {
if (self.cancelBlock) {
self.cancelBlock();
}
}
@end

View File

@@ -0,0 +1,24 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICaptureTimer : NSObject
@property(nonatomic, assign) CGFloat maxCaptureTime;
@property(nonatomic, assign, readonly) CGFloat captureDuration;
@property(nonatomic, copy) void (^progressBlock)(CGFloat ratio, CGFloat recordTime);
@property(nonatomic, copy) void (^progressCancelBlock)(void);
@property(nonatomic, copy) void (^progressFinishBlock)(CGFloat ratio, CGFloat recordTime);
- (void)startTimer;
- (void)stopTimer;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,77 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICaptureTimer.h"
@interface TUICaptureTimer ()
@property(nonatomic, strong) dispatch_source_t gcdTimer;
@property(nonatomic, assign) CGFloat captureDuration;
@end
@implementation TUICaptureTimer
- (instancetype)init {
self = [super init];
if (self) {
self.maxCaptureTime = 15.0f;
}
return self;
}
- (void)startTimer {
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
NSTimeInterval delayTime = 0.f;
NSTimeInterval timeInterval = 0.1f;
dispatch_time_t startDelayTime = dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(delayTime * NSEC_PER_SEC));
dispatch_source_set_timer(self.gcdTimer, startDelayTime, timeInterval * NSEC_PER_SEC, timeInterval * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.gcdTimer, ^{
self.captureDuration += timeInterval;
/**
* Updating UI on the main thread
*/
dispatch_async(dispatch_get_main_queue(), ^{
if (self.progressBlock) {
self.progressBlock(self.captureDuration / self.maxCaptureTime, self.captureDuration);
}
});
/**
* Fnish
*/
if (self.captureDuration >= self.maxCaptureTime) {
/**
* Invalid timer
*/
CGFloat ratio = self.captureDuration / self.maxCaptureTime;
CGFloat recordTime = self.captureDuration;
[self cancel];
dispatch_async(dispatch_get_main_queue(), ^{
if (self.progressFinishBlock) self.progressFinishBlock(ratio, recordTime);
});
}
});
/**
* Start the task. After the GCD timer is created, it needs to be started manually
*/
dispatch_resume(self.gcdTimer);
}
- (void)stopTimer {
[self cancel];
dispatch_async(dispatch_get_main_queue(), ^{
if (self.progressCancelBlock) self.progressCancelBlock();
});
}
- (void)cancel {
if (self.gcdTimer) {
dispatch_source_cancel(self.gcdTimer);
self.gcdTimer = nil;
}
self.captureDuration = 0;
}
@end

View File

@@ -0,0 +1,14 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
@interface TUICaptureVideoPreviewView : UIView
@property(strong, nonatomic) AVCaptureSession *captureSessionsion;
- (CGPoint)captureDevicePointForPoint:(CGPoint)point;
@end

View File

@@ -0,0 +1,37 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICaptureVideoPreviewView.h"
@implementation TUICaptureVideoPreviewView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[(AVCaptureVideoPreviewLayer *)self.layer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
}
return self;
}
- (AVCaptureSession *)captureSessionsion {
return [(AVCaptureVideoPreviewLayer *)self.layer session];
}
- (void)setCaptureSessionsion:(AVCaptureSession *)session {
[(AVCaptureVideoPreviewLayer *)self.layer setSession:session];
}
- (CGPoint)captureDevicePointForPoint:(CGPoint)point {
AVCaptureVideoPreviewLayer *layer = (AVCaptureVideoPreviewLayer *)self.layer;
return [layer captureDevicePointOfInterestForPoint:point];
}
/**
* Make the layer method of the view return the AVCaptureVideoPreviewLayer class object
*/
+ (Class)layerClass {
return [AVCaptureVideoPreviewLayer class];
}
@end

View File

@@ -0,0 +1,22 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICaptureVideoPreviewViewController : UIViewController
@property(nonatomic) void (^commitBlock)(void);
@property(nonatomic) void (^cancelBlock)(void);
- (instancetype)initWithVideoURL:(NSURL *)url NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,136 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICaptureVideoPreviewViewController.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
@import AVFoundation;
@interface TUICaptureVideoPreviewViewController ()
@property(nonatomic) NSURL *fileURL;
@property(nonatomic) AVPlayer *player;
@property(nonatomic) AVPlayerItem *item;
@property(nonatomic) AVPlayerLayer *playerLayer;
@property(nonatomic) UIButton *commitButton;
@property(nonatomic) UIButton *cancelButton;
@property(nonatomic) CGRect lastRect;
@property(nonatomic) BOOL onShow;
@property(nonatomic) BOOL onReadyToPlay;
@end
@implementation TUICaptureVideoPreviewViewController
- (instancetype)initWithVideoURL:(NSURL *)url {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_fileURL = url;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor blackColor];
self.item = [[AVPlayerItem alloc] initWithURL:self.fileURL];
self.player = [[AVPlayer alloc] initWithPlayerItem:self.item];
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
[self.view.layer addSublayer:self.playerLayer];
self.commitButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *commitImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commit")];
[self.commitButton setImage:commitImage forState:UIControlStateNormal];
UIImage *commitBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commitBg")];
[self.commitButton setBackgroundImage:commitBGImage forState:UIControlStateNormal];
[self.commitButton addTarget:self action:@selector(commitButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.commitButton];
self.cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *cancelButtonBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_cancel")];
[self.cancelButton setBackgroundImage:[cancelButtonBGImage rtl_imageFlippedForRightToLeftLayoutDirection] forState:UIControlStateNormal];
[self.cancelButton addTarget:self action:@selector(cancelButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.cancelButton];
[self.item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.item];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *, id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerStatus status = [[change objectForKey:@"new"] intValue];
if (status == AVPlayerStatusReadyToPlay) {
self.onReadyToPlay = YES;
[self playVideo];
}
}
}
- (void)playVideo {
[TUITool dispatchMainAsync:^{
if (self.onShow && self.onReadyToPlay) {
[self.player play];
}
}];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.onShow = YES;
[self playVideo];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (!CGRectEqualToRect(self.lastRect, self.view.bounds)) {
self.lastRect = self.view.bounds;
self.playerLayer.frame = self.view.bounds;
CGFloat commitButtonWidth = 80.0;
CGFloat buttonDistance = (self.view.bounds.size.width - 2 * commitButtonWidth) / 3.0;
CGFloat commitButtonY = self.view.bounds.size.height - commitButtonWidth - 50.0;
CGFloat commitButtonX = 2 * buttonDistance + commitButtonWidth;
self.commitButton.frame = CGRectMake(commitButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
CGFloat cancelButtonX = commitButtonWidth;
self.cancelButton.frame = CGRectMake(cancelButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
if (isRTL()) {
[self.commitButton resetFrameToFitRTL];
[self.cancelButton resetFrameToFitRTL];
}
}
}
- (void)commitButtonClick:(UIButton *)btn {
if (self.commitBlock) {
[self removeObserver];
self.commitBlock();
}
}
- (void)cancelButtonClick:(UIButton *)btn {
if (self.cancelBlock) {
[self removeObserver];
self.cancelBlock();
}
}
- (void)playFinished:(NSNotification *)noti {
[self.player seekToTime:CMTimeMake(0, 1)];
[self.player play];
}
- (void)removeObserver {
[self.item removeObserver:self forKeyPath:@"status"];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end

View File

@@ -0,0 +1,57 @@
//
// TUIChatMediaDataProvider.h
// TUIChat
//
// Created by harvy on 2022/12/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <TIMCommon/TUIMessageCellData.h>
#import "TUIChatMediaSendingManager.h"
NS_ASSUME_NONNULL_BEGIN
typedef void (^TUIChatMediaDataProviderResultCallback)(BOOL success, NSString *__nullable message, NSString *__nullable path);
@protocol TUIChatMediaDataProtocol <NSObject>
- (void)selectPhoto;
- (void)takePicture;
- (void)takeVideo;
- (void)multimediaRecord;
- (void)selectFile;
@end
@protocol TUIChatMediaDataListener <NSObject>
- (void)onProvideImage:(NSString *)imageUrl;
- (void)onProvideImageError:(NSString *)errorMessage;
- (void)onProvideVideo:(NSString *)videoUrl
snapshot:(NSString *)snapshotUrl
duration:(NSInteger)duration
placeHolderCellData:(TUIMessageCellData *)placeHolderCellData;
- (void)onProvidePlaceholderVideoSnapshot:(NSString *)snapshotUrl
SnapImage:(UIImage *)img
Completion:(void (^__nullable)(BOOL finished, TUIMessageCellData *placeHolderCellData))completion;
- (void)onProvideVideoError:(NSString *)errorMessage;
- (void)onProvideFile:(NSString *)fileUrl filename:(NSString *)filename fileSize:(NSInteger)fileSize;
- (void)onProvideFileError:(NSString *)errorMessage;
- (NSString *)currentConversationID;
- (BOOL)isPageAppears;
- (void)sendPlaceHolderUIMessage:(TUIMessageCellData *)cellData;
- (void)sendMessage:(V2TIMMessage *)message placeHolderCellData:(TUIMessageCellData *)placeHolderCellData;
@end
@interface TUIChatMediaDataProvider : NSObject <TUIChatMediaDataProtocol>
@property(nonatomic, weak) UIViewController *presentViewController;
@property(nonatomic, weak) id<TUIChatMediaDataListener> listener;
@property(nonatomic, copy) NSString *conversationID;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,898 @@
//
// TUIChatMediaDataProvider.m
// TUIChat
//
// Created by harvy on 2022/12/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatMediaDataProvider.h"
#import <AssetsLibrary/AssetsLibrary.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <Photos/Photos.h>
#import <PhotosUI/PhotosUI.h>
#import <SDWebImage/SDWebImage.h>
#import <TIMCommon/TIMDefine.h>
#import <TIMCommon/TUIUserAuthorizationCenter.h>
#import <TIMCommon/NSTimer+TUISafe.h>
#import <TUICore/TUITool.h>
#import <TUICore/TUICore.h>
#import "TUICameraViewController.h"
#import "TUIChatConfig.h"
#import "AlbumPicker.h"
#import "MultimediaRecorder.h"
#define kTUIChatMediaSelectImageMax 9
@interface TUIChatMediaDataProvider () <PHPickerViewControllerDelegate,
UINavigationControllerDelegate,
UIImagePickerControllerDelegate,
UIDocumentPickerDelegate,
TUICameraViewControllerDelegate>
@end
@implementation TUIChatMediaDataProvider
#pragma mark - Public API
- (void)selectPhoto {
if ([AlbumPicker sharedInstance].advancedAlbumPicker) {
__weak typeof(self) weakSelf = self;
__strong typeof(weakSelf.listener) strongListener = weakSelf.listener;
[[AlbumPicker sharedInstance].advancedAlbumPicker pickMediaWithCaller:self.presentViewController originalMediaPicked:^(NSDictionary *param) {
if (param) {
NSString * type = param[@"type"];
if ([type isEqualToString:@"image"]) {
// image do nothing
}
else if ([type isEqualToString:@"video"]) {
TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
if ([strongListener respondsToSelector:@selector(sendPlaceHolderUIMessage:)]) {
[strongListener sendPlaceHolderUIMessage:placeHolderCellData];
}
TUIChatMediaTask * task = [[TUIChatMediaTask alloc] init];
task.placeHolderCellData = placeHolderCellData;
task.msgID = placeHolderCellData.msgID;
task.conversationID = weakSelf.conversationID;
if (placeHolderCellData.msgID.length > 0) {
[TUIChatMediaSendingManager.sharedInstance addMediaTask: task forKey:placeHolderCellData.msgID];
}
}
else {
// do nothing
}
}
} progressCallback:^(NSDictionary *param) {
NSLog(@"%@,strongListener:%@",param,strongListener);
} finishedCallback:^(NSDictionary *param) {
if (param) {
V2TIMMessage * message = param[@"message"];
NSString * type = param[@"type"];
if ([type isEqualToString:@"image"]) {
if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
[strongListener sendMessage:message placeHolderCellData:nil];
}
}
else if ([type isEqualToString:@"video"]) {
TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
if (placeHolderCellData.msgID.length > 0) {
[TUIChatMediaSendingManager.sharedInstance removeMediaTaskForKey:placeHolderCellData.msgID];
}
BOOL canSendByCurrentPage = NO;
for (id<TUIChatMediaDataListener> currentVC in TUIChatMediaSendingManager.sharedInstance.mediaSendingControllers) {
if ([currentVC.currentConversationID isEqualToString:self.conversationID]&&
[currentVC respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
if (currentVC.isPageAppears) {
[currentVC sendMessage:message placeHolderCellData:placeHolderCellData];
canSendByCurrentPage = YES;
break;
}
}
}
if (!canSendByCurrentPage) {
if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
[strongListener sendMessage:message placeHolderCellData:placeHolderCellData];
}
}
}
else {
// do nothing
}
}
}];
}
else {
//defalut AlbumPicker
[self _selectPhoto];
}
}
- (void)_selectPhoto {
dispatch_async(dispatch_get_main_queue(), ^{
if (@available(iOS 14.0, *)) {
PHPickerConfiguration *configuration = [[PHPickerConfiguration alloc] init];
configuration.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[ [PHPickerFilter imagesFilter], [PHPickerFilter videosFilter] ]];
configuration.selectionLimit = kTUIChatMediaSelectImageMax;
PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:configuration];
picker.delegate = self;
picker.modalPresentationStyle = UIModalPresentationFullScreen;
picker.view.backgroundColor = [UIColor whiteColor];
[self.presentViewController presentViewController:picker animated:YES completion:nil];
} else {
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
picker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
picker.delegate = self;
[self.presentViewController presentViewController:picker animated:YES completion:nil];
}
}
});
}
- (void)takePicture {
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
[[MultimediaRecorder sharedInstance].advancedVideoRecorder takePhoneWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
NSData *imageData = [NSData dataWithContentsOfURL:uri];
UIImage *photo = [UIImage imageWithData:imageData];
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
[[NSFileManager defaultManager] createFileAtPath:path
contents:UIImagePNGRepresentation(photo) attributes:nil];
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
[self.listener onProvideImage:path];
}
} failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
}];
}
else {
//defalut PhotoCamera
[self _takePicture];
}
}
- (void)_takePicture {
__weak typeof(self) weakSelf = self;
void (^actionBlock)(void) = ^(void) {
TUICameraViewController *vc = [[TUICameraViewController alloc] init];
vc.type = TUICameraMediaTypePhoto;
vc.delegate = weakSelf;
if (weakSelf.presentViewController.navigationController) {
[weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
} else {
[weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
}
};
if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
dispatch_async(dispatch_get_main_queue(), ^{
actionBlock();
});
} else {
if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
[TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
dispatch_async(dispatch_get_main_queue(), ^{
actionBlock();
});
}];
};
}
}
- (void)executeBlockWithMicroAndCameraAuth:(void(^)(void))block{
if ([TUIUserAuthorizationCenter isEnableMicroAuthorization] && [TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
dispatch_async(dispatch_get_main_queue(), block);
} else {
if (![TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
[TUIUserAuthorizationCenter microStateActionWithPopCompletion:^{
if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
dispatch_async(dispatch_get_main_queue(), block);
}
}];
}
if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
[TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
if ([TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
dispatch_async(dispatch_get_main_queue(), block);
}
}];
}
}
}
- (void)takeVideo {
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
[[MultimediaRecorder sharedInstance].advancedVideoRecorder recordVideoWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
if (uri) {
if ([uri.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
[self handleVideoPick:YES message:nil videoUrl:uri];
return;
}
else if ([self isImageURL:uri]){
NSData *imageData = [NSData dataWithContentsOfURL:uri];
UIImage *photo = [UIImage imageWithData:imageData];
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
[[NSFileManager defaultManager] createFileAtPath:path
contents:UIImagePNGRepresentation(photo) attributes:nil];
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
[self.listener onProvideImage:path];
}
}
else {
[self transcodeIfNeed:YES message:nil videoUrl:uri];
}
}
} failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
}];
return;
}
else {
//defalut VideoRecorder
[self _takeVideo];
}
}
- (void)_takeVideo {
__weak typeof(self) weakSelf = self;
void (^actionBlock)(void) = ^(void) {
TUICameraViewController *vc = [[TUICameraViewController alloc] init];
vc.type = TUICameraMediaTypeVideo;
vc.videoMinimumDuration = 1.5;
vc.delegate = weakSelf;
if ([TUIChatConfig defaultConfig].maxVideoRecordDuration > 0) {
vc.videoMaximumDuration = [TUIChatConfig defaultConfig].maxVideoRecordDuration;
}
if (weakSelf.presentViewController.navigationController) {
[weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
} else {
[weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
}
};
[self executeBlockWithMicroAndCameraAuth:actionBlock];
}
- (void)selectFile {
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[ (NSString *)kUTTypeData ]
inMode:UIDocumentPickerModeOpen];
picker.delegate = self;
[self.presentViewController presentViewController:picker animated:YES completion:nil];
}
- (BOOL)isImageURL:(NSURL *)url {
NSArray *imageExtensions = @[@"jpg", @"jpeg", @"png", @"gif", @"bmp", @"tiff", @"webp", @"heic"];
NSString *pathExtension = url.pathExtension.lowercaseString;
return [imageExtensions containsObject:pathExtension];
}
#pragma mark - Private Do task
- (void)handleImagePick:(BOOL)succ message:(NSString *)message imageData:(NSData *)imageData {
static NSDictionary *imageFormatExtensionMap = nil;
if (imageFormatExtensionMap == nil) {
imageFormatExtensionMap = @{
@(SDImageFormatUndefined) : @"",
@(SDImageFormatJPEG) : @"jpeg",
@(SDImageFormatPNG) : @"png",
@(SDImageFormatGIF) : @"gif",
@(SDImageFormatTIFF) : @"tiff",
@(SDImageFormatWebP) : @"webp",
@(SDImageFormatHEIC) : @"heic",
@(SDImageFormatHEIF) : @"heif",
@(SDImageFormatPDF) : @"pdf",
@(SDImageFormatSVG) : @"svg",
@(SDImageFormatBMP) : @"bmp",
@(SDImageFormatRAW) : @"raw"
};
}
dispatch_async(dispatch_get_main_queue(), ^{
if (succ == NO || imageData == nil) {
if ([self.listener respondsToSelector:@selector(onProvideImageError:)]) {
[self.listener onProvideImageError:message];
}
return;
}
UIImage *image = [UIImage imageWithData:imageData];
NSData *data = UIImageJPEGRepresentation(image, 1);
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
NSString *extenionName = [imageFormatExtensionMap objectForKey:@(image.sd_imageFormat)];
if (extenionName.length > 0) {
path = [path stringByAppendingPathExtension:extenionName];
}
int32_t imageFormatSizeMax = 28 * 1024 * 1024;
if (image.sd_imageFormat == SDImageFormatGIF) {
imageFormatSizeMax = 10 * 1024 * 1024;
}
if (imageData.length > imageFormatSizeMax) {
if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
[self.listener onProvideFileError:TIMCommonLocalizableString(TUIKitImageSizeCheckLimited)];
}
return;
}
if (image.sd_imageFormat != SDImageFormatGIF) {
UIImage *newImage = image;
UIImageOrientation imageOrientation = image.imageOrientation;
CGFloat aspectRatio = MIN(1920 / image.size.width, 1920 / image.size.height);
CGFloat aspectWidth = image.size.width * aspectRatio;
CGFloat aspectHeight = image.size.height * aspectRatio;
UIGraphicsBeginImageContext(CGSizeMake(aspectWidth, aspectHeight));
[image drawInRect:CGRectMake(0, 0, aspectWidth, aspectHeight)];
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
data = UIImageJPEGRepresentation(newImage, 0.75);
}
[[NSFileManager defaultManager] createFileAtPath:path contents:data attributes:nil];
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
[self.listener onProvideImage:path];
}
});
}
- (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url {
if (succ == NO || url == nil) {
[self handleVideoPick:NO message:message videoUrl:nil];
return;
}
if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
[self handleVideoPick:succ message:message videoUrl:url];
return;
}
NSString *tempPath = NSTemporaryDirectory();
NSURL *urlName = [url URLByDeletingPathExtension];
NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:newUrl.path]) {
NSError *error;
BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
if (!success || error) {
NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
return;
}
}
// mov to mp4
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
exportSession.outputURL = newUrl;
exportSession.outputFileType = AVFileTypeMPEG4;
exportSession.shouldOptimizeForNetworkUse = YES;
// intercept FirstTime VideoPicture
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
gen.appliesPreferredTrackTransform = YES;
gen.maximumSize = CGSizeMake(192, 192);
NSError *error = nil;
CMTime actualTime;
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
CGImageRelease(imageRef);
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
[self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:image Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusFailed:
NSLog(@"Export session failed");
break;
case AVAssetExportSessionStatusCancelled:
NSLog(@"Export canceled");
break;
case AVAssetExportSessionStatusCompleted: {
// Video conversion finished
NSLog(@"Successful!");
[self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
}
break;
default:
break;
}
}];
[NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
if (exportSession.status == AVAssetExportSessionStatusExporting) {
NSLog(@"exportSession.progress:%f",exportSession.progress);
placeHolderCellData.videoTranscodingProgress = exportSession.progress;
}
}];
}];
}
else {
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusCompleted: {
// Video conversion finished
NSLog(@"Successful!");
[self handleVideoPick:succ message:message videoUrl:newUrl];
} break;
default:
break;
}
}];
}
});
}
- (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url placeHolderCellData:(TUIMessageCellData*)placeHolderCellData {
if (succ == NO || url == nil) {
[self handleVideoPick:NO message:message videoUrl:nil];
return;
}
if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
[self handleVideoPick:succ message:message videoUrl:url];
return;
}
NSString *tempPath = NSTemporaryDirectory();
NSURL *urlName = [url URLByDeletingPathExtension];
NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:newUrl.path]) {
NSError *error;
BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
if (!success || error) {
NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
return;
}
}
// mov to mp4
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
exportSession.outputURL = newUrl;
exportSession.outputFileType = AVFileTypeMPEG4;
exportSession.shouldOptimizeForNetworkUse = YES;
// intercept FirstTime VideoPicture
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
gen.appliesPreferredTrackTransform = YES;
gen.maximumSize = CGSizeMake(192, 192);
NSError *error = nil;
CMTime actualTime;
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
CGImageRelease(imageRef);
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusFailed:
NSLog(@"Export session failed");
break;
case AVAssetExportSessionStatusCancelled:
NSLog(@"Export canceled");
break;
case AVAssetExportSessionStatusCompleted: {
// Video conversion finished
NSLog(@"Successful!");
[self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
}
break;
default:
break;
}
}];
[NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
if (exportSession.status == AVAssetExportSessionStatusExporting) {
NSLog(@"exportSession.progress:%f",exportSession.progress);
placeHolderCellData.videoTranscodingProgress = exportSession.progress;
}
}];
}
else {
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusCompleted: {
// Video conversion finished
NSLog(@"Successful!");
[self handleVideoPick:succ message:message videoUrl:newUrl];
} break;
default:
break;
}
}];
}
});
}
- (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl {
[self handleVideoPick:succ message:message videoUrl:videoUrl placeHolderCellData:nil];
}
- (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl placeHolderCellData:(TUIMessageCellData*)placeHolderCellData{
if (succ == NO || videoUrl == nil) {
if ([self.listener respondsToSelector:@selector(onProvideVideoError:)]) {
[self.listener onProvideVideoError:message];
}
return;
}
NSData *videoData = [NSData dataWithContentsOfURL:videoUrl];
NSString *videoPath = [NSString stringWithFormat:@"%@%@_%u.mp4", TUIKit_Video_Path, [TUITool genVideoName:nil],arc4random()];
[[NSFileManager defaultManager] createFileAtPath:videoPath contents:videoData attributes:nil];
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:videoUrl options:opts];
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
gen.appliesPreferredTrackTransform = YES;
gen.maximumSize = CGSizeMake(192, 192);
NSError *error = nil;
CMTime actualTime;
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
CGImageRelease(imageRef);
NSData *imageData = UIImagePNGRepresentation(image);
NSString *imagePath = [TUIKit_Video_Path stringByAppendingFormat:@"%@_%u",[TUITool genSnapshotName:nil],arc4random()];
[[NSFileManager defaultManager] createFileAtPath:imagePath contents:imageData attributes:nil];
if ([self.listener respondsToSelector:@selector(onProvideVideo:snapshot:duration:placeHolderCellData:)]) {
[self.listener onProvideVideo:videoPath snapshot:imagePath duration:duration placeHolderCellData:placeHolderCellData];
}
}
#pragma mark - PHPickerViewControllerDelegate
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14)) {
dispatch_async(dispatch_get_main_queue(), ^{
[picker dismissViewControllerAnimated:YES completion:nil];
[[[UIApplication sharedApplication] keyWindow] endEditing:YES];
});
if (!results || results.count == 0) {
return;
}
PHPickerResult *result = [results firstObject];
for (PHPickerResult *result in results) {
[self _dealPHPickerResultFinishPicking:result];
}
}
- (void)_dealPHPickerResultFinishPicking:(PHPickerResult *)result API_AVAILABLE(ios(14)) {
NSItemProvider *itemProvoider = result.itemProvider;
__weak typeof(self) weakSelf = self;
if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) {
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeImage
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
BOOL succ = YES;
NSString *message = nil;
if (error) {
succ = NO;
message = error.localizedDescription;
}
[weakSelf handleImagePick:succ message:message imageData:data];
});
}];
} else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMPEG4]) {
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *fileName = @"temp.mp4";
NSString *tempPath = NSTemporaryDirectory();
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
}
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
[weakSelf transcodeIfNeed:flag message:flag ? nil : @"video not found" videoUrl:newUrl];
});
}];
} else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie]) {
// Mov type: screen first
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
[self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:nil Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
// Non-mp4 format video, temporarily use mov suffix, will be converted to mp4 format later
NSDate *datenow = [NSDate date];
NSString *timeSp = [NSString stringWithFormat:@"%ld", (long)([datenow timeIntervalSince1970]*1000)];
NSString *fileName = [NSString stringWithFormat:@"%@_temp.mov",timeSp];
NSString *tempPath = NSTemporaryDirectory();
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
}
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
[weakSelf transcodeIfNeed:flag message:flag ? nil : @"movie not found" videoUrl:newUrl placeHolderCellData:placeHolderCellData];
});
}];
}];
}
} else {
NSString *typeIdentifier = result.itemProvider.registeredTypeIdentifiers.firstObject;
[itemProvoider loadFileRepresentationForTypeIdentifier:typeIdentifier
completionHandler:^(NSURL *_Nullable url, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *result;
NSData *data = [NSData dataWithContentsOfURL:url];
result = [UIImage imageWithData:data];
/**
* Can't get url when typeIdentifier is public.jepg on emulator:
* There is a separate JEPG transcoding issue that only affects the simulator (63426347), please refer to
* https://developer.apple.com/forums/thread/658135 for more information.
*/
});
}];
}
}
#pragma mark - UIImagePickerController
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info {
__weak typeof(self) weakSelf = self;
picker.delegate = nil;
[picker dismissViewControllerAnimated:YES
completion:^{
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
NSURL *url = nil;
if (@available(iOS 11.0, *)) {
url = [info objectForKey:UIImagePickerControllerImageURL];
} else {
url = [info objectForKey:UIImagePickerControllerReferenceURL];
}
BOOL succ = YES;
NSData *imageData = nil;
NSString *errorMessage = nil;
if (url) {
succ = YES;
imageData = [NSData dataWithContentsOfURL:url];
} else {
succ = NO;
errorMessage = @"image not found";
}
[weakSelf handleImagePick:succ message:errorMessage imageData:imageData];
} else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
NSURL *url = [info objectForKey:UIImagePickerControllerMediaURL];
if (url) {
[weakSelf transcodeIfNeed:YES message:nil videoUrl:url];
return;
}
/**
* In some cases UIImagePickerControllerMediaURL may be empty, use UIImagePickerControllerPHAsset
*/
PHAsset *asset = nil;
if (@available(iOS 11.0, *)) {
asset = [info objectForKey:UIImagePickerControllerPHAsset];
}
if (asset) {
[self originURLWithAsset:asset
completion:^(BOOL success, NSURL *URL) {
[weakSelf transcodeIfNeed:success
message:success ? nil : @"origin url with asset not found"
videoUrl:URL];
}];
return;
}
/**
* UIImagePickerControllerPHAsset may be empty, and other methods need to be used to obtain the original path of the video
* file
*/
url = [info objectForKey:UIImagePickerControllerReferenceURL];
if (url) {
[weakSelf originURLWithRefrenceURL:url
completion:^(BOOL success, NSURL *URL) {
[weakSelf transcodeIfNeed:success
message:success ? nil : @"origin url with asset not found"
videoUrl:URL];
}];
return;
}
// not support the video
[weakSelf transcodeIfNeed:NO message:@"not support the video" videoUrl:nil];
}
}];
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[picker dismissViewControllerAnimated:YES completion:nil];
}
/**
* Get the original file path based on UIImagePickerControllerReferenceURL
*/
- (void)originURLWithRefrenceURL:(NSURL *)URL completion:(void (^)(BOOL success, NSURL *URL))completion {
if (completion == nil) {
return;
}
NSDictionary *queryInfo = [self dictionaryWithURLQuery:URL.query];
NSString *fileName = @"temp.mp4";
if ([queryInfo.allKeys containsObject:@"id"] && [queryInfo.allKeys containsObject:@"ext"]) {
fileName = [NSString stringWithFormat:@"%@.%@", queryInfo[@"id"], [queryInfo[@"ext"] lowercaseString]];
}
NSString *tempPath = NSTemporaryDirectory();
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
}
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init];
[assetLibrary assetForURL:URL
resultBlock:^(ALAsset *asset) {
if (asset == nil) {
completion(NO, nil);
return;
}
ALAssetRepresentation *rep = [asset defaultRepresentation];
Byte *buffer = (Byte *)malloc(rep.size);
NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:rep.size error:nil];
NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; // this is NSData may be what you want
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
completion(flag, newUrl);
}
failureBlock:^(NSError *err) {
completion(NO, nil);
}];
}
- (void)originURLWithAsset:(PHAsset *)asset completion:(void (^)(BOOL success, NSURL *URL))completion {
if (completion == nil) {
return;
}
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
if (resources.count == 0) {
completion(NO, nil);
return;
}
PHAssetResourceRequestOptions *options = [[PHAssetResourceRequestOptions alloc] init];
options.networkAccessAllowed = NO;
__block BOOL invoked = NO;
[PHAssetResourceManager.defaultManager requestDataForAssetResource:resources.firstObject
options:options
dataReceivedHandler:^(NSData *_Nonnull data) {
/**
*
* There will be a problem of repeated callbacks here
*/
if (invoked) {
return;
}
invoked = YES;
if (data == nil) {
completion(NO, nil);
return;
}
NSString *fileName = @"temp.mp4";
NSString *tempPath = NSTemporaryDirectory();
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
}
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
completion(flag, newUrl);
}
completionHandler:^(NSError *_Nullable error) {
completion(NO, nil);
}];
}
- (NSDictionary *)dictionaryWithURLQuery:(NSString *)query {
NSArray *components = [query componentsSeparatedByString:@"&"];
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (NSString *item in components) {
NSArray *subs = [item componentsSeparatedByString:@"="];
if (subs.count == 2) {
[dict setObject:subs.lastObject forKey:subs.firstObject];
}
}
return [NSDictionary dictionaryWithDictionary:dict];
;
}
#pragma mark - TUICameraViewControllerDelegate
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithVideoURL:(NSURL *)url {
[self transcodeIfNeed:YES message:nil videoUrl:url];
}
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithImageData:(NSData *)data {
[self handleImagePick:YES message:nil imageData:data];
}
- (void)cameraViewControllerDidCancel:(TUICameraViewController *)controller {
}
- (void)cameraViewControllerDidPictureLib:(TUICameraViewController *)controller finishCallback:(void (^)(void))callback {
[self selectPhoto];
if (callback) {
callback();
}
}
#pragma mark - UIDocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {
[url startAccessingSecurityScopedResource];
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init];
NSError *error;
@weakify(self);
[coordinator
coordinateReadingItemAtURL:url
options:0
error:&error
byAccessor:^(NSURL *newURL) {
@strongify(self);
NSData *fileData = [NSData dataWithContentsOfURL:newURL options:NSDataReadingMappedIfSafe error:nil];
NSString *fileName = [url lastPathComponent];
NSString *filePath = [TUIKit_File_Path stringByAppendingString:fileName];
if (fileData.length > 1e9 || fileData.length == 0) { // 1e9 bytes = 1GB
UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitFileSizeCheckLimited) message:nil preferredStyle:UIAlertControllerStyleAlert];
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Confirm) style:UIAlertActionStyleDefault handler:nil]];
[self.presentViewController presentViewController:ac animated:YES completion:nil];
return;
}
if ([NSFileManager.defaultManager fileExistsAtPath:filePath]) {
/**
* If a file with the same name exists, increment the file name
*/
int i = 0;
NSArray *arrayM = [NSFileManager.defaultManager subpathsAtPath:TUIKit_File_Path];
for (NSString *sub in arrayM) {
if ([sub.pathExtension isEqualToString:fileName.pathExtension] &&
[sub.stringByDeletingPathExtension tui_containsString:fileName.stringByDeletingPathExtension]) {
i++;
}
}
if (i) {
fileName = [fileName
stringByReplacingOccurrencesOfString:fileName.stringByDeletingPathExtension
withString:[NSString stringWithFormat:@"%@(%d)", fileName.stringByDeletingPathExtension, i]];
filePath = [TUIKit_File_Path stringByAppendingString:fileName];
}
}
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] fileSize];
if ([self.listener respondsToSelector:@selector(onProvideFile:filename:fileSize:)]) {
[self.listener onProvideFile:filePath filename:fileName fileSize:fileSize];
}
} else {
if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
[self.listener onProvideFileError:@"file not found"];
}
}
}];
[url stopAccessingSecurityScopedResource];
[controller dismissViewControllerAnimated:YES completion:nil];
}
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
[controller dismissViewControllerAnimated:YES completion:nil];
}
@end

View File

@@ -0,0 +1,14 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
@import UIKit;
@interface TUIMotionManager : NSObject
@property(nonatomic, assign) UIDeviceOrientation deviceOrientation;
@property(nonatomic, assign) AVCaptureVideoOrientation videoOrientation;
@end

View File

@@ -0,0 +1,61 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIMotionManager.h"
#import <CoreMotion/CoreMotion.h>
@import UIKit;
@interface TUIMotionManager ()
@property(nonatomic, strong) CMMotionManager *motionManager;
@end
@implementation TUIMotionManager
- (instancetype)init {
self = [super init];
if (self) {
_motionManager = [[CMMotionManager alloc] init];
_motionManager.deviceMotionUpdateInterval = 1 / 15.0;
if (!_motionManager.deviceMotionAvailable) {
_motionManager = nil;
return self;
}
__weak __typeof(self) weakSelf = self;
[_motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
withHandler:^(CMDeviceMotion *motion, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf performSelectorOnMainThread:@selector(handleDeviceMotion:) withObject:motion waitUntilDone:YES];
}];
}
return self;
}
- (void)handleDeviceMotion:(CMDeviceMotion *)deviceMotion {
double x = deviceMotion.gravity.x;
double y = deviceMotion.gravity.y;
if (fabs(y) >= fabs(x)) {
if (y >= 0) {
_deviceOrientation = UIDeviceOrientationPortraitUpsideDown;
_videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
} else {
_deviceOrientation = UIDeviceOrientationPortrait;
_videoOrientation = AVCaptureVideoOrientationPortrait;
}
} else {
if (x >= 0) {
_deviceOrientation = UIDeviceOrientationLandscapeRight;
_videoOrientation = AVCaptureVideoOrientationLandscapeRight;
} else {
_deviceOrientation = UIDeviceOrientationLandscapeLeft;
_videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
}
}
}
- (void)dealloc {
[_motionManager stopDeviceMotionUpdates];
}
@end

View File

@@ -0,0 +1,26 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIMovieManager : NSObject
@property(nonatomic, assign) AVCaptureVideoOrientation referenceOrientation;
@property(nonatomic, assign) AVCaptureVideoOrientation currentOrientation;
@property(nonatomic, strong) AVCaptureDevice *currentDevice;
- (void)start:(void (^)(NSError *error))handle;
- (void)stop:(void (^)(NSURL *url, NSError *error))handle;
- (void)writeData:(AVCaptureConnection *)connection video:(AVCaptureConnection *)video audio:(AVCaptureConnection *)audio buffer:(CMSampleBufferRef)buffer;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,235 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIMovieManager.h"
#import <TIMCommon/TIMDefine.h>
@interface TUIMovieManager () {
BOOL _readyToRecordVideo;
BOOL _readyToRecordAudio;
dispatch_queue_t _movieWritingQueue;
NSURL *_movieURL;
AVAssetWriter *_movieWriter;
AVAssetWriterInput *_movieAudioInput;
AVAssetWriterInput *_movieVideoInput;
}
@end
@implementation TUIMovieManager
- (instancetype)init {
self = [super init];
if (self) {
_movieWritingQueue = dispatch_queue_create("com.tui.Movie.Writing.Queue", DISPATCH_QUEUE_SERIAL);
_movieURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"TUICaptureTempMovie.mp4"]];
_referenceOrientation = AVCaptureVideoOrientationPortrait;
}
return self;
}
- (void)start:(void (^)(NSError *error))handle {
@weakify(self);
dispatch_async(_movieWritingQueue, ^{
@strongify(self);
[self removeFile:self->_movieURL];
NSError *error;
if (!self->_movieWriter) {
self->_movieWriter = [[AVAssetWriter alloc] initWithURL:self->_movieURL fileType:AVFileTypeMPEG4 error:&error];
}
handle(error);
});
}
- (void)stop:(void (^)(NSURL *url, NSError *error))handle {
@weakify(self);
dispatch_async(_movieWritingQueue, ^{
@strongify(self);
self->_readyToRecordVideo = NO;
self->_readyToRecordAudio = NO;
if (self->_movieWriter && self->_movieWriter.status == AVAssetWriterStatusWriting) {
@weakify(self);
[self->_movieWriter finishWritingWithCompletionHandler:^() {
@strongify(self);
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
if (self->_movieWriter.status == AVAssetWriterStatusCompleted) {
handle(self->_movieURL, nil);
} else {
handle(nil, self->_movieWriter.error);
}
self->_movieWriter = nil;
});
}];
} else {
[self->_movieWriter cancelWriting];
self->_movieWriter = nil;
dispatch_async(dispatch_get_main_queue(), ^{
handle(nil, [NSError errorWithDomain:@"com.tui.Movie.Writing" code:0 userInfo:@{NSLocalizedDescriptionKey : @"AVAssetWriter status error"}]);
});
}
});
}
- (void)writeData:(AVCaptureConnection *)connection video:(AVCaptureConnection *)video audio:(AVCaptureConnection *)audio buffer:(CMSampleBufferRef)buffer {
CFRetain(buffer);
@weakify(self);
dispatch_async(_movieWritingQueue, ^{
@strongify(self);
if (connection == video) {
if (!self->_readyToRecordVideo) {
self->_readyToRecordVideo = [self setupAssetWriterVideoInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
}
if ([self inputsReadyToRecord]) {
[self writeSampleBuffer:buffer ofType:AVMediaTypeVideo];
}
} else if (connection == audio) {
if (!self->_readyToRecordAudio) {
self->_readyToRecordAudio = [self setupAssetWriterAudioInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
}
if ([self inputsReadyToRecord]) {
[self writeSampleBuffer:buffer ofType:AVMediaTypeAudio];
}
}
CFRelease(buffer);
});
}
- (void)writeSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(NSString *)mediaType {
if (_movieWriter.status == AVAssetWriterStatusUnknown) {
if ([_movieWriter startWriting]) {
[_movieWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
} else {
NSLog(@"%@", _movieWriter.error);
}
}
if (_movieWriter.status == AVAssetWriterStatusWriting) {
if (mediaType == AVMediaTypeVideo) {
if (!_movieVideoInput.isReadyForMoreMediaData) {
return;
}
if (![_movieVideoInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"%@", _movieWriter.error);
}
} else if (mediaType == AVMediaTypeAudio) {
if (!_movieAudioInput.isReadyForMoreMediaData) {
return;
}
if (![_movieAudioInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"%@", _movieWriter.error);
}
}
}
}
- (BOOL)inputsReadyToRecord {
return _readyToRecordVideo && _readyToRecordAudio;
}
- (NSError *)setupAssetWriterAudioInput:(CMFormatDescriptionRef)currentFormatDescription {
size_t aclSize = 0;
const AudioStreamBasicDescription *currentASBD = CMAudioFormatDescriptionGetStreamBasicDescription(currentFormatDescription);
const AudioChannelLayout *channelLayout = CMAudioFormatDescriptionGetChannelLayout(currentFormatDescription, &aclSize);
NSData *dataLayout = aclSize > 0 ? [NSData dataWithBytes:channelLayout length:aclSize] : [NSData data];
NSDictionary *settings = @{
AVFormatIDKey : [NSNumber numberWithInteger:kAudioFormatMPEG4AAC],
AVSampleRateKey : [NSNumber numberWithFloat:currentASBD->mSampleRate],
AVChannelLayoutKey : dataLayout,
AVNumberOfChannelsKey : [NSNumber numberWithInteger:currentASBD->mChannelsPerFrame],
AVEncoderBitRatePerChannelKey : [NSNumber numberWithInt:64000]
};
if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeAudio]) {
_movieAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:settings];
_movieAudioInput.expectsMediaDataInRealTime = YES;
if ([_movieWriter canAddInput:_movieAudioInput]) {
[_movieWriter addInput:_movieAudioInput];
} else {
return _movieWriter.error;
}
} else {
return _movieWriter.error;
}
return nil;
}
- (NSError *)setupAssetWriterVideoInput:(CMFormatDescriptionRef)currentFormatDescription {
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(currentFormatDescription);
NSUInteger numPixels = dimensions.width * dimensions.height;
CGFloat bitsPerPixel = numPixels < (640 * 480) ? 4.05 : 11.0;
NSDictionary *compression =
@{AVVideoAverageBitRateKey : [NSNumber numberWithInteger:numPixels * bitsPerPixel], AVVideoMaxKeyFrameIntervalKey : [NSNumber numberWithInteger:30]};
NSDictionary *settings = @{
AVVideoCodecKey : AVVideoCodecH264,
AVVideoWidthKey : [NSNumber numberWithInteger:dimensions.width],
AVVideoHeightKey : [NSNumber numberWithInteger:dimensions.height],
AVVideoCompressionPropertiesKey : compression
};
if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeVideo]) {
_movieVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:settings];
_movieVideoInput.expectsMediaDataInRealTime = YES;
_movieVideoInput.transform = [self transformFromCurrentVideoOrientationToOrientation:self.referenceOrientation];
if ([_movieWriter canAddInput:_movieVideoInput]) {
[_movieWriter addInput:_movieVideoInput];
} else {
return _movieWriter.error;
}
} else {
return _movieWriter.error;
}
return nil;
}
- (CGAffineTransform)transformFromCurrentVideoOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
CGFloat orientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:orientation];
CGFloat videoOrientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:self.currentOrientation];
CGFloat angleOffset;
if (self.currentDevice.position == AVCaptureDevicePositionBack) {
angleOffset = videoOrientationAngleOffset - orientationAngleOffset + M_PI_2;
} else {
angleOffset = orientationAngleOffset - videoOrientationAngleOffset + M_PI_2;
}
CGAffineTransform transform = CGAffineTransformMakeRotation(angleOffset);
return transform;
}
- (CGFloat)angleOffsetFromPortraitOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
CGFloat angle = 0.0;
switch (orientation) {
case AVCaptureVideoOrientationPortrait:
angle = 0.0;
break;
case AVCaptureVideoOrientationPortraitUpsideDown:
angle = M_PI;
break;
case AVCaptureVideoOrientationLandscapeRight:
angle = -M_PI_2;
break;
case AVCaptureVideoOrientationLandscapeLeft:
angle = M_PI_2;
break;
}
return angle;
}
- (void)removeFile:(NSURL *)fileURL {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *filePath = fileURL.path;
if ([fileManager fileExistsAtPath:filePath]) {
NSError *error;
BOOL success = [fileManager removeItemAtPath:filePath error:&error];
if (!success) {
NSAssert(NO, error.localizedDescription);
NSLog(@"Failed to delete file%@", error);
} else {
NSLog(@"Succeed to delete file");
}
}
}
@end

View File

@@ -0,0 +1,34 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
*
* This document declares the relevant modules for group request management.
* You can manage users' group join requests through the TUIGroupPendencyController in this file.
* Including browsing applicant information, processing applicant requests and other related operations.
*/
#import <TIMCommon/TIMDefine.h>
#import <UIKit/UIKit.h>
#import "TUIGroupPendencyDataProvider.h"
NS_ASSUME_NONNULL_BEGIN
/**
*
* 【Module name】 TUIGroupPendencyController
* 【Function description】Group request controller.
* This view is responsible for providing the group administrator with a controller for processing group addition applications when the group is set to
* "Require Admin Approval" This control is implemented by UITableView by default, and the application for group membership is displayed through tableView. The
* information for joining a group application includes: user avatar, user nickname, application introduction, and agree button. After clicking a specific
* tableCell, you can enter the detailed interface corresponding to the application (the detailed page includes a reject button).
*/
@interface TUIGroupPendencyController : UITableViewController
@property TUIGroupPendencyDataProvider *viewModel;
@property(nonatomic, copy) void (^cellClickBlock)(TUIGroupPendencyCell *cell);
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,87 @@
//
// TUIGroupPendencyController.m
// TXIMSDK_TUIKit_iOS
//
// Created by annidyfeng on 2019/6/18.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIGroupPendencyController.h"
#import <TIMCommon/TIMDefine.h>
@interface TUIGroupPendencyController ()
@end
@implementation TUIGroupPendencyController
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[TUIGroupPendencyCell class] forCellReuseIdentifier:@"PendencyCell"];
self.tableView.tableFooterView = [UIView new];
self.title = TIMCommonLocalizableString(TUIKitGroupApplicant);
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.viewModel.dataList.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TUIGroupPendencyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PendencyCell" forIndexPath:indexPath];
TUIGroupPendencyCellData *data = self.viewModel.dataList[indexPath.row];
data.cselector = @selector(cellClick:);
data.cbuttonSelector = @selector(btnClick:);
[cell fillWithData:data];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
return NO;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 70;
}
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// add code here for when you hit delete
[self.tableView beginUpdates];
TUIGroupPendencyCellData *data = self.viewModel.dataList[indexPath.row];
[self.viewModel removeData:data];
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
}
- (void)btnClick:(TUIGroupPendencyCell *)cell {
[self.viewModel acceptData:cell.pendencyData];
[self.tableView reloadData];
}
- (void)cellClick:(TUIGroupPendencyCell *)cell {
if (self.cellClickBlock) {
self.cellClickBlock(cell);
}
}
@end

View File

@@ -0,0 +1,41 @@
//
// TUIGroupPinCell.h
// TUIChat
//
// Created by Tencent on 2024/05/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/NSDictionary+TUISafe.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUILogin.h>
#import <TIMCommon/TUIBubbleMessageCellData.h>
#import <TIMCommon/TUIMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIGroupPinCellView : UIView
@property (nonatomic, copy) void(^onClickRemove)(V2TIMMessage *originMessage);
@property (nonatomic, copy) void(^onClickCellView)(V2TIMMessage *originMessage);
@property (nonatomic, strong) TUIMessageCellData *cellData;
@property (nonatomic, strong) UIImageView *leftIcon;
@property (nonatomic, strong) UILabel * titleLabel;
@property (nonatomic, strong) UILabel * content;
@property (nonatomic, strong) UIButton * removeButton;
@property (nonatomic, strong) UIView * multiAnimationView;
@property (nonatomic, strong) UIView * bottomLine;
@property (nonatomic, assign) BOOL isFirstPage;
- (void)fillWithData:(TUIMessageCellData *)cellData;
- (void)hiddenMultiAnimation;
- (void)showMultiAnimation;
@end
@interface TUIGroupPinCell : UITableViewCell
@property (nonatomic,strong) TUIGroupPinCellView* cellView;
- (void)fillWithData:(TUIMessageCellData *)cellData;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,297 @@
//
// TUIGroupPinCell.m
// TUIChat
//
// Created by Tencent on 2024/05/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMessageDataProvider.h"
#import "TUIGroupPinCell.h"
@implementation TUIGroupPinCellView
- (instancetype)init {
self = [super init];
if (self) {
[self setupView];
}
return self;
}
- (void)fillWithData:(TUIMessageCellData *)cellData {
self.cellData = cellData;
self.titleLabel.text = [TUIMessageDataProvider getShowName:cellData.innerMessage];
self.content.text = [TUIMessageDataProvider getDisplayString:cellData.innerMessage];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.leftIcon mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self);
make.centerY.mas_equalTo(self);
make.width.mas_equalTo(6);
make.top.bottom.mas_equalTo(self);
}];
[self.titleLabel sizeToFit];
[self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.leftIcon.mas_trailing).mas_offset(8);
make.trailing.mas_lessThanOrEqualTo(self.removeButton.mas_leading);
make.width.mas_equalTo(self.titleLabel.frame.size.width);
make.height.mas_equalTo(self.titleLabel.frame.size.height);
make.top.mas_equalTo(self).mas_offset(9);
}];
[self.content sizeToFit];
[self.content mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.leftIcon.mas_trailing).mas_offset(8);
make.trailing.mas_lessThanOrEqualTo(self.removeButton.mas_leading);
make.width.mas_equalTo(self.content.frame.size.width);
make.height.mas_equalTo(self.content.frame.size.height);
make.bottom.mas_equalTo(self).mas_offset(-9);
}];
[self.removeButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(-10);
make.centerY.mas_equalTo(self);
make.width.mas_equalTo(30);
make.height.mas_equalTo(30);
}];
[self.removeButton.imageView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self.removeButton);
make.width.mas_equalTo(14);
make.height.mas_equalTo(14);
}];
[self.multiAnimationView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.mas_leading);
make.trailing.mas_equalTo(self.mas_trailing);
make.width.mas_equalTo(self);
make.height.mas_equalTo(20);
make.top.mas_equalTo(self.mas_bottom);
}];
[self.bottomLine mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(self);
make.height.mas_equalTo(0.5);
make.centerX.mas_equalTo(self);
make.bottom.mas_equalTo(self);
}];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint newP = [self convertPoint:point toView:self.multiAnimationView];
if ([self.multiAnimationView pointInside:newP withEvent:event]) {
return self.multiAnimationView;
}
return [super hitTest:point withEvent:event];
}
- (void)setupView {
self.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_back_color", @"#F9F9F9");
[self addSubview:self.leftIcon];
[self addSubview:self.titleLabel];
[self addSubview:self.content];
[self addSubview:self.removeButton];
[self addSubview:self.multiAnimationView];
[self addSubview:self.bottomLine];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
[self addGestureRecognizer:tap];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (UIImageView *)leftIcon {
if (!_leftIcon) {
_leftIcon = [[UIImageView alloc] init];
_leftIcon.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_left_color", @"#D9D9D9");
}
return _leftIcon;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.textColor = TUIChatDynamicColor(@"chat_pop_group_pin_title_color", @"#141516");
_titleLabel.font = [UIFont systemFontOfSize:16.0];
}
return _titleLabel;
}
- (UILabel *)content {
if (!_content) {
_content = [[UILabel alloc] init];
_content.textColor = [TUIChatDynamicColor(@"chat_pop_group_pin_subtitle_color", @"#000000")
colorWithAlphaComponent:0.6];
_content.font = [UIFont systemFontOfSize:14.0];
}
return _content;
}
- (UIButton *)removeButton {
if (!_removeButton) {
_removeButton = [UIButton buttonWithType:UIButtonTypeCustom];;
[_removeButton setImage:[UIImage imageNamed:TUIChatImagePath(@"chat_group_del_icon")] forState:UIControlStateNormal];
[_removeButton addTarget:self action:@selector(removeCurrentGroupPin) forControlEvents:UIControlEventTouchUpInside];
}
return _removeButton;
}
- (UIView *)multiAnimationView {
if (!_multiAnimationView) {
_multiAnimationView = [[UIView alloc] initWithFrame:CGRectZero];
_multiAnimationView.alpha = 0;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
_multiAnimationView.userInteractionEnabled = YES;
[_multiAnimationView addGestureRecognizer:tap];
UIView *arrowBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
arrowBackgroundView.backgroundColor = [UIColor clearColor];
arrowBackgroundView.layer.cornerRadius = 5;
[_multiAnimationView addSubview:arrowBackgroundView];
_multiAnimationView.clipsToBounds = YES;
UIImageView *arrow = [[UIImageView alloc] initWithFrame:CGRectZero];
arrow.image = TUIChatBundleThemeImage(@"chat_pop_group_pin_down_arrow_img", @"chat_down_arrow_icon");
[arrowBackgroundView addSubview:arrow];
UIView *bottomLine = [[UIView alloc] init];
bottomLine.backgroundColor = [UIColor grayColor];
[arrowBackgroundView addSubview:bottomLine];
[arrowBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(_multiAnimationView);
make.size.mas_equalTo(CGSizeMake(20, 20));
}];
[arrow mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(arrowBackgroundView);
make.size.mas_equalTo(CGSizeMake(20, 20));
}];
[bottomLine mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(_multiAnimationView);
make.height.mas_equalTo(0.5);
make.centerX.mas_equalTo(_multiAnimationView);
make.bottom.mas_equalTo(_multiAnimationView);
}];
}
return _multiAnimationView;
}
- (UIView *)bottomLine {
if (!_bottomLine) {
_bottomLine = [[UIView alloc] init];
_bottomLine.backgroundColor = [UIColor grayColor];
}
return _bottomLine;
}
- (void)removeCurrentGroupPin {
if (self.onClickRemove) {
self.onClickRemove(self.cellData.innerMessage);
}
}
- (void)onTap:(id)sender {
if (self.onClickCellView) {
self.onClickCellView(self.cellData.innerMessage);
}
}
- (void)hiddenMultiAnimation {
self.multiAnimationView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0];
_multiAnimationView.alpha = 0;
_bottomLine.alpha = 1;
}
- (void)showMultiAnimation {
self.multiAnimationView.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_back_color", @"#F9F9F9");
_multiAnimationView.alpha = 1;
_bottomLine.alpha = 0;
}
@end
@interface TUIGroupPinCell ()
@property (nonatomic, strong) UIView *separatorView;
@end
@implementation TUIGroupPinCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
[self setupView];
}
return self;
}
- (void)setupView {
[self.contentView addSubview:self.cellView];
[self.contentView addSubview:self.separatorView];
}
- (TUIGroupPinCellView *)cellView {
if (!_cellView) {
_cellView = [[TUIGroupPinCellView alloc] init];
_cellView.isFirstPage = NO;
}
return _cellView;
}
- (UIView *)separatorView {
if (!_separatorView) {
_separatorView = [[UIView alloc] init];
_separatorView.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_line_color", @"#DDDDDD");
}
return _separatorView;
}
- (void)fillWithData:(TUIMessageCellData *)cellData {
[self.cellView fillWithData:cellData];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.cellView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self);
make.trailing.mas_equalTo(self);
make.top.mas_equalTo(self);
make.bottom.mas_equalTo(self);
}];
[self.separatorView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.contentView).mas_offset(6);
make.trailing.mas_equalTo(self.contentView);
make.bottom.mas_equalTo(self.contentView);
make.height.mas_equalTo(0.5);
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
@end

View File

@@ -0,0 +1,23 @@
//
// TUIGroupPinPageViewController.h
// TUIChat
//
// Created by Tencent on 2024/05/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
@import ImSDK_Plus;
NS_ASSUME_NONNULL_BEGIN
@interface TUIGroupPinPageViewController : UIViewController
@property(nonatomic, strong) UITableView *tableview;
@property(nonatomic, strong) UIView *customArrowView;
@property(nonatomic, strong) NSArray *groupPinList;
@property (nonatomic, copy) void(^onClickRemove)(V2TIMMessage *originMessage);
@property (nonatomic, copy) void(^onClickCellView)(V2TIMMessage *originMessage);
@property(nonatomic, assign) BOOL canRemove;
@property (nonatomic,strong) UIView *bottomShadow;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,155 @@
//
// TUIGroupPinPageViewController.m
// TUIChat
//
// Created by Tencent on 2024/05/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIGroupPinPageViewController.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/NSDictionary+TUISafe.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUILogin.h>
#import "TUIGroupPinCell.h"
#import "TUIMessageDataProvider.h"
@interface TUIGroupPinPageViewController() <UITableViewDelegate,UITableViewDataSource>
@end
@implementation TUIGroupPinPageViewController
- (instancetype)init {
self = [super init];
if (self) {
self.tableview.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_back_color", @"#F9F9F9");
self.customArrowView.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_back_color", @"#F9F9F9");
self.bottomShadow.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:.5];
[self addSingleTapGesture];
}
return self;
}
- (void)addSingleTapGesture {
// When clicking on the shadow, the page disappears
self.view.userInteractionEnabled = YES;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)];
[self.view addGestureRecognizer:singleTap];
}
- (void)singleTap:(UITapGestureRecognizer *)tap {
[UIView animateWithDuration:0.3 animations:^{
self.tableview.frame = CGRectMake(0, self.tableview.frame.origin.y, self.view.frame.size.width, 60);
self.customArrowView.frame = CGRectMake(0, CGRectGetMaxY(self.tableview.frame), self.view.frame.size.width, self.customArrowView.frame.size.height);
self.bottomShadow.frame = CGRectMake(0, CGRectGetMaxY(self.customArrowView.frame), self.view.frame.size.width, 0);
}completion:^(BOOL finished) {
if (finished) {
[self dismissViewControllerAnimated:NO completion:nil];
}
}];
}
- (UITableView *)tableview {
if (!_tableview) {
_tableview = [[UITableView alloc] init];
_tableview.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
_tableview.delegate = self;
_tableview.dataSource = self;
[_tableview setSeparatorStyle:UITableViewCellSeparatorStyleNone];
_tableview.showsVerticalScrollIndicator = NO;
_tableview.showsHorizontalScrollIndicator = NO;
[self.view addSubview:_tableview];
UIView *herderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 0)];
_tableview.tableHeaderView = herderView;
}
return _tableview;
}
- (UIView *)customArrowView {
if (!_customArrowView) {
_customArrowView = [[UIView alloc] initWithFrame:CGRectMake(0, _tableview.frame.size.height, _tableview.frame.size.width, 100)];
[self.view addSubview:_customArrowView];
UIView *arrowBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
arrowBackgroundView.backgroundColor = [UIColor clearColor];
arrowBackgroundView.layer.cornerRadius = 5;
[_customArrowView addSubview:arrowBackgroundView];
_customArrowView.clipsToBounds = YES;
UIImageView *arrow = [[UIImageView alloc] initWithFrame:CGRectZero];
arrow.image = TUIChatBundleThemeImage(@"chat_pop_group_pin_up_arrow_img", @"chat_up_arrow_icon");
[arrowBackgroundView addSubview:arrow];
[arrowBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(_customArrowView);
make.size.mas_equalTo(CGSizeMake(20, 20));
}];
[arrow mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(arrowBackgroundView);
make.size.mas_equalTo(CGSizeMake(20, 20));
}];
}
return _customArrowView;
}
- (UIView *)bottomShadow {
if (!_bottomShadow) {
_bottomShadow = [[UIView alloc] init];
_bottomShadow.userInteractionEnabled = NO;
[self.view addSubview:_bottomShadow];
}
return _bottomShadow;
}
#pragma mark - group pin
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TUIGroupPinCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil) {
cell = [[TUIGroupPinCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
}
cell.backgroundColor = [UIColor systemGroupedBackgroundColor];
V2TIMMessage *msg = self.groupPinList[indexPath.row];
TUIMessageCellData * cellData = [TUIMessageDataProvider getCellData:msg];
[cell fillWithData:cellData];
__weak __typeof(self) weakSelf = self;
cell.cellView.removeButton.hidden = !self.canRemove;
cell.cellView.onClickRemove = ^(V2TIMMessage *originMessage) {
if (weakSelf.onClickRemove) {
weakSelf.onClickRemove(originMessage);
}
};
cell.cellView.onClickCellView = ^(V2TIMMessage *originMessage) {
if (weakSelf.onClickCellView) {
weakSelf.onClickCellView(originMessage);
}
[weakSelf singleTap:nil];
};
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.groupPinList.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat contentH = 62;
CGFloat padding = 0;
return contentH + padding;
}
@end

View File

@@ -0,0 +1,29 @@
//
// TUIChatFlexViewController.h
// TUIChat
//
// Created by wyl on 2022/10/27.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TIMDefine.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIChatFlexViewController : UIViewController
@property(nonatomic, strong) UIView *topGestureView;
@property(nonatomic, strong) UIImageView *topImgView;
@property(nonatomic, strong) UIView *containerView;
- (void)updateSubContainerView;
- (void)setnormalTop;
- (void)setNormalBottom;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,163 @@
//
// TUIChatFlexViewController.m
// TUIChat
//
// Created by wyl on 2022/10/27.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatFlexViewController.h"
typedef enum : NSUInteger {
FLEX_TOP,
FLEX_Bottom,
} FLEX_Location;
CGFloat topMargin = NavBar_Height + 30;
@interface TUIChatFlexViewController ()
@property(nonatomic, assign) FLEX_Location currentLoaction;
@property(nonatomic, strong) UIPanGestureRecognizer *panCover;
@property(nonatomic, strong) UITapGestureRecognizer *singleTap;
@end
@implementation TUIChatFlexViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:.5];
self.containerView.backgroundColor = [UIColor whiteColor];
self.topImgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:TUIChatImagePath_Minimalist(@"icon_flex_arrow")]];
[self.topGestureView addSubview:self.topImgView];
[self addSingleTapGesture];
if (!_currentLoaction) {
self.currentLoaction = FLEX_TOP;
}
[self updateSubContainerView];
}
- (void)addSingleTapGesture {
// When clicking on the shadow, the page disappears
self.view.userInteractionEnabled = YES;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)];
singleTap.cancelsTouchesInView = NO;
[self.view addGestureRecognizer:singleTap];
}
- (void)singleTap:(UITapGestureRecognizer *)tap {
CGPoint translation = [tap locationInView:self.containerView];
if (translation.x < 0 || translation.y < 0) {
[self dismissViewControllerAnimated:YES completion:nil];
} else if (translation.x > self.containerView.frame.size.width || translation.y > self.containerView.frame.size.height) {
[self dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)setnormalTop {
self.currentLoaction = FLEX_TOP;
}
- (void)setNormalBottom {
self.currentLoaction = FLEX_Bottom;
}
- (void)setCurrentLoaction:(FLEX_Location)currentLoaction {
_currentLoaction = currentLoaction;
if (currentLoaction == FLEX_TOP) {
self.containerView.frame = CGRectMake(0, topMargin, self.view.frame.size.width, self.view.frame.size.height - topMargin);
} else if (currentLoaction == FLEX_Bottom) {
self.containerView.frame = CGRectMake(0, self.view.frame.size.height - kScale390(393), self.view.frame.size.width, kScale390(393));
}
}
#pragma mark - lazy
- (UIView *)containerView {
if (_containerView == nil) {
_containerView = [[UIView alloc] init];
_containerView.layer.cornerRadius = kScale390(12);
[self.view addSubview:_containerView];
}
return _containerView;
}
- (UIView *)topGestureView {
if (_topGestureView == nil) {
_topGestureView = [[UIView alloc] init];
[_topGestureView addGestureRecognizer:self.panCover];
[self.containerView addSubview:_topGestureView];
}
return _topGestureView;
}
- (UIPanGestureRecognizer *)panCover {
if (_panCover == nil) {
_panCover = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onPanCover:)];
}
return _panCover;
}
- (void)onPanCover:(UIPanGestureRecognizer *)pan {
CGPoint translation = [pan translationInView:self.topGestureView];
CGFloat absX = fabs(translation.x);
CGFloat absY = fabs(translation.y);
if (MAX(absX, absY) < 2) return;
if (absX > absY) {
if (translation.x < 0) {
// scroll left
} else {
// scroll right
}
} else if (absY > absX) {
if (translation.y < 0) {
// scroll up
[self.topGestureView removeGestureRecognizer:self.panCover];
[UIView animateWithDuration:0.3
animations:^{
self.currentLoaction = FLEX_TOP;
[self.topGestureView addGestureRecognizer:self.panCover];
}
completion:^(BOOL finished) {
if (finished) {
[self updateSubContainerView];
}
}];
} else {
// scroll down
if (self.currentLoaction == FLEX_Bottom) {
[self dismissViewControllerAnimated:YES completion:nil];
}
[self.topGestureView removeGestureRecognizer:self.panCover];
[UIView animateWithDuration:0.3
animations:^{
self.currentLoaction = FLEX_Bottom;
[self.topGestureView addGestureRecognizer:self.panCover];
}
completion:^(BOOL finished) {
if (finished) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self updateSubContainerView];
});
}
}];
}
}
}
- (void)updateSubContainerView {
self.topGestureView.frame = CGRectMake(0, 0, self.containerView.frame.size.width, kScale390(40));
self.topImgView.frame = CGRectMake((self.topGestureView.frame.size.width - kScale390(24)) * 0.5, kScale390(22), kScale390(24), kScale390(6));
}
@end

View File

@@ -0,0 +1,48 @@
//
// TUIChatPopContextController.h
// TUIChat
//
// Created by wyl on 2022/10/24.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TUIMessageCellData.h>
#import <UIKit/UIKit.h>
#import "TUIChatPopContextExtionView.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, BlurEffectStyle) {
BlurEffectStyleLight,
BlurEffectStyleExtraLight,
BlurEffectStyleDarkEffect,
};
@interface TUIChatPopContextController : UIViewController
@property(nonatomic, strong) Class alertCellClass;
@property(nonatomic, strong) TUIMessageCellData *alertViewCellData;
@property(nonatomic, assign) CGRect originFrame;
@property(copy, nonatomic) void (^viewWillShowHandler)(TUIMessageCell *alertView);
@property(copy, nonatomic) void (^viewDidShowHandler)(TUIMessageCell *alertView);
// dismiss controller completed block
@property(nonatomic, copy) void (^dismissComplete)(void);
@property(nonatomic, copy) void (^reactClickCallback)(NSString *faceName);
@property(nonatomic, strong) NSMutableArray<TUIChatPopContextExtionItem *> *items;
- (void)setBlurEffectWithView:(UIView *)view;
- (void)blurDismissViewControllerAnimated:(BOOL)animated completion:(void (^__nullable)(BOOL finished))completion;
- (void)updateExtionView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,462 @@
//
// TUIChatPopContextController.m
// TUIChat
//
// Created by wyl on 2022/10/24.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatPopContextController.h"
#import <TIMCommon/TIMDefine.h>
#import "UIImage+ImageEffects.h"
#import <TUICore/TUICore.h>
@interface TUIChatPopContextController ()<V2TIMAdvancedMsgListener>
@property(nonatomic, strong) UIView *recentView;
@property(nonatomic, strong) UIView *alertContainerView;
@property(nonatomic, strong) TUIMessageCell *alertView;
@property(nonatomic, strong) TUIChatPopContextExtionView *extionView;
@property(nonatomic, strong) UIColor *backgroundColor; // set backgroundColor
@property(nonatomic, strong) UIView *backgroundView; // you set coustom view to it
@property(nonatomic, strong) UITapGestureRecognizer *singleTap;
@property(nonatomic, assign) BOOL backgoundTapDismissEnable; // default NO
@end
@implementation TUIChatPopContextController
- (instancetype)init {
if (self = [super init]) {
[self configureController];
}
return self;
}
- (void)configureController {
self.providesPresentationContextTransitionStyle = YES;
self.definesPresentationContext = YES;
self.modalPresentationStyle = UIModalPresentationCustom;
_backgroundColor = [UIColor clearColor];
_backgoundTapDismissEnable = YES;
[[V2TIMManager sharedInstance] addAdvancedMsgListener:self];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor clearColor];
[self addBackgroundView];
[self addSingleTapGesture];
[self configureAlertView];
[self configRecentView];
[self configExtionView];
[self.view layoutIfNeeded];
[self showHapticFeedback];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (_viewWillShowHandler) {
_viewWillShowHandler(_alertView);
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (_viewDidShowHandler) {
_viewDidShowHandler(_alertView);
}
//
// Too far to the top
CGFloat moveY = 0;
if (self.recentView.frame.origin.y < NavBar_Height) {
CGFloat deal = NavBar_Height - self.recentView.frame.origin.y;
moveY = deal + NavBar_Height + 50;
}
//
// Too far to the right
CGFloat moveX = 0;
if (self.recentView.frame.origin.x + self.recentView.frame.size.width > self.view.frame.size.width) {
CGFloat deal = self.recentView.frame.origin.x + self.recentView.frame.size.width - self.view.frame.size.width;
moveX = deal + 5;
}
//
// too far down
if (self.extionView.frame.origin.y + self.extionView.frame.size.height > self.view.frame.size.height) {
CGFloat deal = self.extionView.frame.origin.y + self.extionView.frame.size.height - self.view.frame.size.height;
moveY = -deal - 50;
}
BOOL oneScreenCanFillCheck = NO;
// Can only one screen fit
if (self.recentView.frame.size.height + self.originFrame.size.height + self.extionView.frame.size.height + kScale390(100) > self.view.bounds.size.height) {
oneScreenCanFillCheck = YES;
}
if (oneScreenCanFillCheck) {
// recentView
CGFloat recentViewMoveY = NavBar_Height + 50;
self.recentView.frame =
CGRectMake(self.recentView.frame.origin.x - moveX, recentViewMoveY, self.recentView.frame.size.width, self.recentView.frame.size.height);
// alertView
[UIView animateWithDuration:0.3
animations:^{
self.alertContainerView.frame =
CGRectMake(0, self.recentView.frame.origin.y + kScale390(8) + self.recentView.frame.size.height,
self.view.frame.size.width, self.originFrame.size.height);
}
completion:^(BOOL finished){
}];
// extionView
CGFloat deal = self.extionView.frame.origin.y + self.extionView.frame.size.height - self.view.frame.size.height;
CGFloat extionViewMoveY = -deal - 50;
self.extionView.frame = CGRectMake(self.extionView.frame.origin.x - moveX, self.extionView.frame.origin.y + extionViewMoveY,
self.extionView.frame.size.width, self.extionView.frame.size.height);
self.extionView.transform = CGAffineTransformMakeScale(0.1, 0.1);
[UIView animateWithDuration:0.5
animations:^{
// Bounces
self.extionView.transform = CGAffineTransformMakeScale(1, 1);
}
completion:^(BOOL finished){
}];
return;
} else {
// When the container need a displacement change
// Or do nothing
if (moveY != 0) {
[UIView animateWithDuration:0.3
animations:^{
self.alertContainerView.frame = CGRectMake(0, self.originFrame.origin.y + moveY, self.view.frame.size.width, self.originFrame.size.height);
}
completion:^(BOOL finished){
}];
}
self.recentView.frame = CGRectMake(self.recentView.frame.origin.x - moveX, self.recentView.frame.origin.y, self.recentView.frame.size.width,
self.recentView.frame.size.height);
[UIView animateWithDuration:0.2
animations:^{
// When recentView needs to have displacement animation
self.recentView.frame = CGRectMake(self.recentView.frame.origin.x, self.recentView.frame.origin.y + moveY,
self.recentView.frame.size.width, self.recentView.frame.size.height);
}
completion:^(BOOL finished){
}];
self.extionView.frame = CGRectMake(self.extionView.frame.origin.x - moveX, self.extionView.frame.origin.y + moveY, self.extionView.frame.size.width,
self.extionView.frame.size.height);
self.extionView.transform = CGAffineTransformMakeScale(0.1, 0.1);
[UIView animateWithDuration:0.5
animations:^{
// Bounces
self.extionView.transform = CGAffineTransformMakeScale(1, 1);
}
completion:^(BOOL finished){
}];
}
}
- (void)addBackgroundView {
if (_backgroundView == nil) {
UIView *backgroundView = [[UIView alloc] init];
backgroundView.backgroundColor = _backgroundColor;
_backgroundView = backgroundView;
}
_backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view insertSubview:_backgroundView atIndex:0];
[self addConstraintToView:_backgroundView edgeInset:UIEdgeInsetsZero];
}
- (void)setBackgroundView:(UIView *)backgroundView {
if (_backgroundView == nil) {
_backgroundView = backgroundView;
} else if (_backgroundView != backgroundView) {
backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view insertSubview:backgroundView aboveSubview:_backgroundView];
[self addConstraintToView:backgroundView edgeInset:UIEdgeInsetsZero];
backgroundView.alpha = 0;
[UIView animateWithDuration:0.3
animations:^{
backgroundView.alpha = 1;
}
completion:^(BOOL finished) {
[_backgroundView removeFromSuperview];
_backgroundView = backgroundView;
[self addSingleTapGesture];
}];
}
}
- (void)addSingleTapGesture {
self.view.userInteractionEnabled = YES;
_backgroundView.userInteractionEnabled = YES;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)];
singleTap.enabled = _backgoundTapDismissEnable;
[_backgroundView addGestureRecognizer:singleTap];
_singleTap = singleTap;
}
- (void)configureAlertView {
self.alertContainerView = [[UIView alloc] init];
[self.view addSubview:self.alertContainerView];
_alertView = [[self.alertCellClass alloc] init];
[self.alertContainerView addSubview:_alertView];
_alertView.userInteractionEnabled = YES;
if ([self.alertView isKindOfClass:NSClassFromString(@"TUIMergeMessageCell_Minimalist")]) {
_alertView.userInteractionEnabled = NO;
}
[_alertView fillWithData:self.alertViewCellData];
[_alertView layoutIfNeeded];
self.alertContainerView.frame = CGRectMake(0, _originFrame.origin.y, self.view.frame.size.width, _originFrame.size.height);
_alertView.frame = CGRectMake(0, 0, self.alertContainerView.frame.size.width, self.alertContainerView.frame.size.height);
for (UIView *view in _alertView.contentView.subviews) {
if(view != _alertView.container) {
view.hidden = YES;
}
}
[_alertView.container mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(_originFrame.origin.x);
make.top.mas_equalTo(0);
make.size.mas_equalTo(_originFrame.size);
}];
}
- (void)configRecentView {
_recentView = [[UIView alloc] init];
_recentView.backgroundColor = [UIColor clearColor];
[self.view addSubview:_recentView];
_recentView.frame = CGRectMake(_originFrame.origin.x,
_originFrame.origin.y - kScale390(8 + 40),
MAX(kTIMDefaultEmojiSize.width *8,kScale390(208)),
kScale390(40));
NSDictionary *param = @{TUICore_TUIChatExtension_ChatPopMenuReactRecentView_Delegate : self};
[TUICore raiseExtension:TUICore_TUIChatExtension_ChatPopMenuReactRecentView_MinimalistExtensionID parentView:self.recentView param:param];
}
- (void)configExtionView {
_extionView = [[TUIChatPopContextExtionView alloc] init];
_extionView.backgroundColor = [UIColor tui_colorWithHex:@"f9f9f9"];
_extionView.layer.cornerRadius = kScale390(16);
[self.view addSubview:_extionView];
CGFloat height = [self configAndCaculateExtionHeight];
_extionView.frame = CGRectMake(_originFrame.origin.x, _originFrame.origin.y + _originFrame.size.height + kScale390(8), kScale390(180), height);
}
- (CGFloat)configAndCaculateExtionHeight {
NSMutableArray *items = self.items;
CGFloat height = 0;
for (int i = 0; i < items.count; i++) {
height += kScale390(40);
}
CGFloat topMargin = kScale390(6);
CGFloat bottomMargin = kScale390(6);
height += topMargin;
height += bottomMargin;
[_extionView configUIWithItems:items topBottomMargin:topMargin];
return height;
}
- (void)updateExtionView {
CGFloat height = [self configAndCaculateExtionHeight];
_extionView.frame = CGRectMake(_extionView.frame.origin.x, _extionView.frame.origin.y, _extionView.frame.size.width, height);
}
- (void)setBackgoundTapDismissEnable:(BOOL)backgoundTapDismissEnable {
_backgoundTapDismissEnable = backgoundTapDismissEnable;
_singleTap.enabled = backgoundTapDismissEnable;
}
- (void)addConstraintToView:(UIView *)view edgeInset:(UIEdgeInsets)edgeInset {
[self addConstraintWithView:view topView:self.view leftView:self.view bottomView:self.view rightView:self.view edgeInset:edgeInset];
}
- (void)addConstraintWithView:(UIView *)view
topView:(UIView *)topView
leftView:(UIView *)leftView
bottomView:(UIView *)bottomView
rightView:(UIView *)rightView
edgeInset:(UIEdgeInsets)edgeInset {
if (topView) {
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:topView
attribute:NSLayoutAttributeTop
multiplier:1
constant:edgeInset.top]];
}
if (leftView) {
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:leftView
attribute:NSLayoutAttributeLeft
multiplier:1
constant:edgeInset.left]];
}
if (rightView) {
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:rightView
attribute:NSLayoutAttributeRight
multiplier:1
constant:edgeInset.right]];
}
if (bottomView) {
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:bottomView
attribute:NSLayoutAttributeBottom
multiplier:1
constant:edgeInset.bottom]];
}
}
#pragma mark - public
- (void)setBlurEffectWithView:(UIView *)view {
[self setBlurEffectWithView:view style:BlurEffectStyleDarkEffect];
}
- (void)setBlurEffectWithView:(UIView *)view style:(BlurEffectStyle)blurStyle {
UIImage *snapshotImage = [UIImage snapshotImageWithView:view];
UIImage *blurImage = [self blurImageWithSnapshotImage:snapshotImage style:blurStyle];
dispatch_async(dispatch_get_main_queue(), ^{
UIImageView *blurImageView = [[UIImageView alloc] initWithImage:blurImage];
self.backgroundView = blurImageView;
});
}
- (void)setBlurEffectWithView:(UIView *)view effectTintColor:(UIColor *)effectTintColor {
UIImage *snapshotImage = [UIImage snapshotImageWithView:view];
UIImage *blurImage = [snapshotImage applyTintEffectWithColor:effectTintColor];
UIImageView *blurImageView = [[UIImageView alloc] initWithImage:blurImage];
self.backgroundView = blurImageView;
}
#pragma mark - private
- (void)showHapticFeedback {
if (@available(iOS 10.0, *)) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
[generator prepare];
[generator impactOccurred];
});
} else {
// Fallback on earlier versions
}
}
- (UIImage *)blurImageWithSnapshotImage:(UIImage *)snapshotImage style:(BlurEffectStyle)blurStyle {
switch (blurStyle) {
case BlurEffectStyleLight:
return [snapshotImage applyLightEffect];
case BlurEffectStyleDarkEffect:
return [snapshotImage applyDarkEffect];
case BlurEffectStyleExtraLight:
return [snapshotImage applyExtraLightEffect];
default:
return nil;
}
}
- (void)blurDismissViewControllerAnimated:(BOOL)animated completion:(void (^__nullable)(BOOL finished))completion {
[self dismissViewControllerAnimated:animated];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (completion) {
completion(YES);
}
});
}
- (void)dismissViewControllerAnimated:(BOOL)animated {
[UIView animateWithDuration:0.3
animations:^{
self.recentView.frame = CGRectMake(self.recentView.frame.origin.x, self.originFrame.origin.y - self.recentView.frame.size.height,
self.recentView.frame.size.width, self.recentView.frame.size.height);
}
completion:^(BOOL finished){
}];
[UIView animateWithDuration:0.3
animations:^{
self.alertContainerView.frame = CGRectMake(0, self.originFrame.origin.y , self.view.frame.size.width, self.originFrame.size.height);
}
completion:^(BOOL finished) {
if (finished) {
[self dismissViewControllerAnimated:animated completion:self.dismissComplete];
}
}];
self.extionView.transform = CGAffineTransformMakeScale(1, 1);
[UIView animateWithDuration:0.2
animations:^{
self.extionView.transform = CGAffineTransformMakeScale(0.1, 0.1);
}
completion:^(BOOL finished) {
if (finished) {
self.extionView.transform = CGAffineTransformMakeScale(0, 0);
}
}];
}
#pragma mark - action
- (void)singleTap:(UITapGestureRecognizer *)sender {
[self dismissViewControllerAnimated:NO];
}
// MARK: V2TIMAdvancedMsgListener
- (void)onRecvMessageRevoked:(NSString *)msgID operateUser:(V2TIMUserFullInfo *)operateUser reason:(NSString *)reason {
if ([msgID isEqualToString:self.alertViewCellData.msgID]) {
UIViewController *controller = self;
while(controller.presentingViewController != nil){
controller = controller.presentingViewController;
}
[controller dismissViewControllerAnimated:YES completion:^{
[self blurDismissViewControllerAnimated:NO completion:nil];
}];
}
}
@end

View File

@@ -0,0 +1,40 @@
//
// TUIChatPopContextExtionView.h
// TUIEmojiPlugin
//
// Created by wyl on 2023/12/1.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIChatPopContextExtionItem : NSObject
@property(nonatomic, copy) NSString *title;
@property(nonatomic, strong) UIColor *titleColor;
@property(nonatomic, strong) UIFont *titleFont;
@property(nonatomic, assign) CGFloat weight;
@property(nonatomic, strong) UIImage *markIcon;
@property(nonatomic, assign) CGFloat itemHeight;
@property(nonatomic, assign) BOOL needBottomLine;
@property(nonatomic, copy) void (^actionHandler)(TUIChatPopContextExtionItem *item);
- (instancetype)initWithTitle:(NSString *)title markIcon:(UIImage *)markIcon weight:(NSInteger)weight withActionHandler:(void (^)(id action))actionHandler;
@end
@interface TUIChatPopContextExtionView : UIView
- (void)configUIWithItems:(NSMutableArray<TUIChatPopContextExtionItem *> *)items topBottomMargin:(CGFloat)topBottomMargin;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,114 @@
//
// TUIChatPopContextExtionView.m
// TUIEmojiPlugin
//
// Created by wyl on 2023/12/1.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatPopContextExtionView.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import <TIMCommon/TUIFitButton.h>
#import <TIMCommon/TIMCommonMediator.h>
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
@implementation TUIChatPopContextExtionItem
- (instancetype)initWithTitle:(NSString *)title markIcon:(UIImage *)markIcon weight:(NSInteger)weight withActionHandler:(void (^)(id action))actionHandler {
self = [super init];
if (self) {
_title = title;
_markIcon = markIcon;
_weight = weight;
_actionHandler = actionHandler;
}
return self;
}
@end
@interface TUIChatPopContextExtionItemView : UIView
@property(nonatomic, strong) TUIChatPopContextExtionItem *item;
@property(nonatomic, strong) UIImageView *icon;
@property(nonatomic, strong) UILabel *l;
- (void)configBaseUIWithItem:(TUIChatPopContextExtionItem *)item;
@end
@implementation TUIChatPopContextExtionItemView
- (void)configBaseUIWithItem:(TUIChatPopContextExtionItem *)item {
self.item = item;
CGFloat itemWidth = self.frame.size.width;
CGFloat padding = kScale390(16);
CGFloat itemHeight = self.frame.size.height;
UIImageView *icon = [[UIImageView alloc] init];
[self addSubview:icon];
icon.frame = CGRectMake(itemWidth - padding - kScale390(18), itemHeight * 0.5 - kScale390(18) * 0.5, kScale390(18), kScale390(18));
icon.image = self.item.markIcon;
UILabel *l = [[UILabel alloc] init];
l.frame = CGRectMake(padding, 0, itemWidth * 0.5, itemHeight);
l.text = self.item.title;
l.font = item.titleFont ?: [UIFont systemFontOfSize:kScale390(16)];
l.textAlignment = isRTL()? NSTextAlignmentRight:NSTextAlignmentLeft;
l.textColor = item.titleColor ?: [UIColor blackColor];
l.userInteractionEnabled = false;
[self addSubview:l];
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeSystem];
[backButton addTarget:self action:@selector(buttonclick) forControlEvents:UIControlEventTouchUpInside];
backButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
backButton.frame = CGRectMake(0, 0, itemWidth, itemHeight);
[self addSubview:backButton];
if (item.needBottomLine) {
UIView *line = [UIView new];
line.backgroundColor = [UIColor tui_colorWithHex:@"DDDDDD"];
line.frame = CGRectMake(0, itemHeight - kScale390(0.5), itemWidth, kScale390(0.5));
[self addSubview:line];
}
self.layer.masksToBounds = YES;
if (isRTL()) {
for (UIView *subview in self.subviews) {
[subview resetFrameToFitRTL];
}
}
}
- (void)buttonclick {
if (self.item.actionHandler) {
self.item.actionHandler(self.item);
}
}
@end
@interface TUIChatPopContextExtionView ()
@property(nonatomic, strong) NSMutableArray<TUIChatPopContextExtionItem *> *items;
@end
@implementation TUIChatPopContextExtionView
- (void)configUIWithItems:(NSMutableArray<TUIChatPopContextExtionItem *> *)items topBottomMargin:(CGFloat)topBottomMargin {
if (self.subviews.count > 0) {
for (UIView *subview in self.subviews) {
if (subview) {
[subview removeFromSuperview];
}
}
}
int i = 0;
for (TUIChatPopContextExtionItem *item in items) {
TUIChatPopContextExtionItemView *itemView = [[TUIChatPopContextExtionItemView alloc] init];
itemView.frame = CGRectMake(0, (kScale390(40)) * i + topBottomMargin, kScale390(180), kScale390(40));
[itemView configBaseUIWithItem:item];
[self addSubview:itemView];
i++;
}
}
@end

View File

@@ -0,0 +1,30 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/*
File: UIImage+ImageEffects.h
Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code youll want to look out to find out how
to use vImage to efficiently calculate a blur. Version: 1.0
*/
@import UIKit;
@interface UIImage (ImageEffects)
- (UIImage *)applyLightEffect;
- (UIImage *)applyExtraLightEffect;
- (UIImage *)applyDarkEffect;
- (UIImage *)applyTintEffectWithColor:(UIColor *)tintColor;
- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius
tintColor:(UIColor *)tintColor
saturationDeltaFactor:(CGFloat)saturationDeltaFactor
maskImage:(UIImage *)maskImage;
@end
@interface UIImage (SnapshotImage)
+ (UIImage *)snapshotImageWithView:(UIView *)view;
@end

View File

@@ -0,0 +1,202 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "UIImage+ImageEffects.h"
@import Accelerate;
#import <float.h>
@implementation UIImage (ImageEffects)
- (UIImage *)applyLightEffect {
UIColor *tintColor = [UIColor colorWithWhite:1.0 alpha:0.3];
return [self applyBlurWithRadius:10 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
- (UIImage *)applyExtraLightEffect {
UIColor *tintColor = [UIColor colorWithWhite:0.97 alpha:0.82];
return [self applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
- (UIImage *)applyDarkEffect {
UIColor *tintColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4];
return [self applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
- (UIImage *)applyTintEffectWithColor:(UIColor *)tintColor {
const CGFloat effectColorAlpha = 0.6;
UIColor *effectColor = tintColor;
int componentCount = CGColorGetNumberOfComponents(tintColor.CGColor);
if (componentCount == 2) {
CGFloat b;
if ([tintColor getWhite:&b alpha:NULL]) {
effectColor = [UIColor colorWithWhite:b alpha:effectColorAlpha];
}
} else {
CGFloat r, g, b;
if ([tintColor getRed:&r green:&g blue:&b alpha:NULL]) {
effectColor = [UIColor colorWithRed:r green:g blue:b alpha:effectColorAlpha];
}
}
return [self applyBlurWithRadius:10 tintColor:effectColor saturationDeltaFactor:-1.0 maskImage:nil];
}
- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius
tintColor:(UIColor *)tintColor
saturationDeltaFactor:(CGFloat)saturationDeltaFactor
maskImage:(UIImage *)maskImage {
// Check pre-conditions.
if (self.size.width < 1 || self.size.height < 1) {
NSLog(@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", self.size.width, self.size.height, self);
return nil;
}
if (!self.CGImage) {
NSLog(@"*** error: image must be backed by a CGImage: %@", self);
return nil;
}
if (maskImage && !maskImage.CGImage) {
NSLog(@"*** error: maskImage must be backed by a CGImage: %@", maskImage);
return nil;
}
CGRect imageRect = {CGPointZero, self.size};
UIImage *effectImage = self;
BOOL hasBlur = blurRadius > __FLT_EPSILON__;
BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__;
if (hasBlur || hasSaturationChange) {
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef effectInContext = UIGraphicsGetCurrentContext();
CGContextScaleCTM(effectInContext, 1.0, -1.0);
CGContextTranslateCTM(effectInContext, 0, -self.size.height);
CGContextDrawImage(effectInContext, imageRect, self.CGImage);
vImage_Buffer effectInBuffer;
effectInBuffer.data = CGBitmapContextGetData(effectInContext);
effectInBuffer.width = CGBitmapContextGetWidth(effectInContext);
effectInBuffer.height = CGBitmapContextGetHeight(effectInContext);
effectInBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectInContext);
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef effectOutContext = UIGraphicsGetCurrentContext();
vImage_Buffer effectOutBuffer;
effectOutBuffer.data = CGBitmapContextGetData(effectOutContext);
effectOutBuffer.width = CGBitmapContextGetWidth(effectOutContext);
effectOutBuffer.height = CGBitmapContextGetHeight(effectOutContext);
effectOutBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectOutContext);
if (hasBlur) {
// A description of how to compute the box kernel width from the Gaussian
// radius (aka standard deviation) appears in the SVG spec:
// http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
//
// For larger values of 's' (s >= 2.0), an approximation can be used: Three
// successive box-blurs build a piece-wise quadratic convolution kernel, which
// approximates the Gaussian kernel to within roughly 3%.
//
// let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
//
// ... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
//
CGFloat inputRadius = blurRadius * [[UIScreen mainScreen] scale];
NSUInteger radius = floor(inputRadius * 3. * sqrt(2 * M_PI) / 4 + 0.5);
if (radius % 2 != 1) {
radius += 1; // force radius to be odd so that the three box-blur methodology works.
}
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
}
BOOL effectImageBuffersAreSwapped = NO;
if (hasSaturationChange) {
CGFloat s = saturationDeltaFactor;
CGFloat floatingPointSaturationMatrix[] = {
0.0722 + 0.9278 * s,
0.0722 - 0.0722 * s,
0.0722 - 0.0722 * s,
0,
0.7152 - 0.7152 * s,
0.7152 + 0.2848 * s,
0.7152 - 0.7152 * s,
0,
0.2126 - 0.2126 * s,
0.2126 - 0.2126 * s,
0.2126 + 0.7873 * s,
0,
0,
0,
0,
1,
};
const int32_t divisor = 256;
NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix) / sizeof(floatingPointSaturationMatrix[0]);
int16_t saturationMatrix[matrixSize];
for (NSUInteger i = 0; i < matrixSize; ++i) {
saturationMatrix[i] = (int16_t)roundf(floatingPointSaturationMatrix[i] * divisor);
}
if (hasBlur) {
vImageMatrixMultiply_ARGB8888(&effectOutBuffer, &effectInBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
effectImageBuffersAreSwapped = YES;
} else {
vImageMatrixMultiply_ARGB8888(&effectInBuffer, &effectOutBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
}
}
if (!effectImageBuffersAreSwapped) effectImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (effectImageBuffersAreSwapped) effectImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
// Set up output context.
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef outputContext = UIGraphicsGetCurrentContext();
CGContextScaleCTM(outputContext, 1.0, -1.0);
CGContextTranslateCTM(outputContext, 0, -self.size.height);
// Draw base image.
CGContextDrawImage(outputContext, imageRect, self.CGImage);
// Draw effect image.
if (hasBlur) {
CGContextSaveGState(outputContext);
if (maskImage) {
CGContextClipToMask(outputContext, imageRect, maskImage.CGImage);
}
CGContextDrawImage(outputContext, imageRect, effectImage.CGImage);
CGContextRestoreGState(outputContext);
}
// Add in color tint.
if (tintColor) {
CGContextSaveGState(outputContext);
CGContextSetFillColorWithColor(outputContext, tintColor.CGColor);
CGContextFillRect(outputContext, imageRect);
CGContextRestoreGState(outputContext);
}
// Output image is ready.
UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return outputImage;
}
@end
@implementation UIImage (SnapshotImage)
+ (UIImage *)snapshotImageWithView:(UIView *)view {
// currentView The current view creates a bitmap-based graphics context and specifies the size of
UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, [UIScreen mainScreen].scale);
// renderInContext Renders the receiver and its subscopes to the specified context
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO];
// Returns an image based on the current graphics context
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// generated image
UIGraphicsEndImageContext();
return image;
}
@end