├── BugError └── BugError.swift ├── iOS_App_Template ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── ViewController.h ├── AppDelegate.h ├── iOS_App_Template.entitlements ├── main.m ├── ViewController.m ├── Info.plist ├── Base.lproj │ ├── Main.storyboard │ └── LaunchScreen.storyboard └── AppDelegate.m ├── Template ├── DownloadUploadBackupCommon │ ├── UIImage+Format.h │ ├── XMPhotosRequestManager+Utils.h │ ├── TransferDSADDelegate.h │ ├── DeviceNetworkManager.h │ ├── TransferCell.h │ ├── FileTask+WCTTableCoding.h │ ├── User+WCTTableCoding.h │ ├── UIImage+Format.m │ ├── User.h │ ├── XMPhotosRequestManager+Utils.m │ ├── FileTask.h │ ├── TransferCell.m │ ├── DeviceNetworkManager.m │ ├── User.mm │ └── FileTask.mm ├── SpeechUtils.h ├── ShouldNotAutorotate │ ├── UIViewController+ShouldNotAutorotate.h │ └── UIViewController+ShouldNotAutorotate.m ├── Upload │ ├── UploadDSAD.h │ └── UploadManager.h ├── Download │ ├── DownloadDSAD.h │ └── DownloadManager.h ├── MDNS │ ├── MDNSManager.h │ └── MDNSManager.m ├── BLE │ └── BLEUtils.h ├── Webview │ ├── URLSchemeHandler.h │ └── URLSchemeHandler.m ├── Backup │ └── BackupManager.h ├── LaunchViewController.m ├── Test2ViewController.swift ├── TableViewCellAutoCalculate.m ├── SpeechUtils.m ├── SelectFileViewController.m ├── LocalAuthentication.m ├── ServiceRegister.m ├── SwiftTemplate.swift ├── AlertViewController.m ├── Share.m ├── TableViewTemplate.m ├── TemplateUtils.m ├── EventKitTemplate.m └── IPAddr.m ├── iOS_App_Template.xcodeproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Utils ├── NSInputStreamSkip │ ├── NSInputStream+Skip.h │ └── NSInputStream+Skip.m ├── Toast.h ├── LocalizedManager │ ├── SupportLanguages.plist │ ├── LocalizedManager.h │ └── LocalizedManager.m ├── Utils.h ├── _XMAutoLaunch.m ├── InputLimiter │ ├── InputLimiter.h │ └── InputLimiter.m ├── Const.h ├── XMLock.h └── Toast.m ├── Development_ExportOptions.plist ├── .gitignore ├── README.md └── iOS_App_Template-archive.sh /BugError/BugError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class BugError { 4 | public func com() { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS_App_Template/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/UIImage+Format.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Format.h 3 | 4 | #import 5 | 6 | @class PHAsset; 7 | 8 | @interface UIImage (Format) 9 | 10 | + (BOOL)isHEIF:(PHAsset *)asset; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /iOS_App_Template.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS_App_Template/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // iOS_App_Template 4 | // 5 | // Created by mxm on 2018/1/22. 6 | // Copyright © 2019 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /iOS_App_Template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Template/SpeechUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // SpeechUtils.h 3 | // iOS_App_Template 4 | // 5 | // Created by macmini on 2022/1/6. 6 | // Copyright © 2022 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface SpeechUtils : NSObject 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Template/ShouldNotAutorotate/UIViewController+ShouldNotAutorotate.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+ShouldNotAutorotate.h 3 | 4 | #import 5 | 6 | /** 使用此协议后,不要在子类中重写shouldAutorotate和supportedInterfaceOrientations方法,否则旋转的控制就会失效 */ 7 | @protocol ShouldNotAutorotate @end 8 | 9 | @interface UIViewController (ShouldNotAutorotate) 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/XMPhotosRequestManager+Utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // XMPhotosRequestManager+Utils.h 3 | //https://github.com/mxmhao/XMPhotosRequestManager 4 | 5 | #import "XMPhotosRequestManager.h" 6 | 7 | @interface XMPhotosRequestManager (Utils) 8 | 9 | + (AVMutableVideoComposition *)fixedCompositionWithAsset:(AVAsset *)videoAsset; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /iOS_App_Template/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // iOS_App_Template 4 | // 5 | // Created by mxm on 2018/1/22. 6 | // Copyright © 2019 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /iOS_App_Template/iOS_App_Template.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.HotspotConfiguration 6 | 7 | com.apple.developer.networking.wifi-info 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Utils/NSInputStreamSkip/NSInputStream+Skip.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSInputStream+Skip.h 3 | // SourceTest 4 | // 5 | // Created by min on 2019/10/20. 6 | // Copyright © 2019 min. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface NSInputStream (Skip) 14 | 15 | - (void)skip:(NSUInteger)byteCount; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /iOS_App_Template/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // iOS_App_Template 4 | // 5 | // Created by mxm on 2018/1/22. 6 | // Copyright © 2019 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Template/Upload/UploadDSAD.h: -------------------------------------------------------------------------------- 1 | // 2 | // UploadDSAD.h 3 | 4 | #import 5 | #import "TransferDSADDelegate.h" 6 | 7 | @interface UploadDSAD : NSObject 8 | 9 | @property (nonatomic, weak) id delegate; 10 | 11 | - (BOOL)selectAllTasks; 12 | 13 | - (void)deselectAllTasks:(BOOL)animated; 14 | 15 | - (void)deleteAllSelected; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Utils/Toast.h: -------------------------------------------------------------------------------- 1 | // 2 | // Toast.h 3 | // RadarModule 4 | // 5 | // Created by macmini on 2023/9/26. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface Toast : NSObject 13 | 14 | - (void)addToastViewTo:(UIView *)view; 15 | 16 | - (void)addToastViewOnCenterTo:(UIView *)view; 17 | 18 | - (void)removeToastViewFromSuperview; 19 | 20 | - (void)show:(NSString *)text; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /Template/Download/DownloadDSAD.h: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadDSAD.h 3 | // DSAS: DataSource and Delegate 4 | 5 | #import 6 | #import "TransferDSADDelegate.h" 7 | 8 | @interface DownloadDSAD : NSObject 9 | 10 | @property (nonatomic, weak) id delegate; 11 | 12 | - (BOOL)selectAllTasks; 13 | 14 | - (void)deselectAllTasks:(BOOL)animated; 15 | 16 | - (void)deleteAllSelected; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/TransferDSADDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // TransferDSADDelegate.h 3 | 4 | #import 5 | 6 | @class FileTask, UIAlertController; 7 | 8 | @protocol TransferDSADDelegate 9 | @optional 10 | //全选 11 | - (void)didSelectAll:(BOOL)isAllSelected; 12 | //没有选中的 13 | - (void)noneSelected:(BOOL)isNone; 14 | //打开文件 15 | - (void)openFile:(FileTask *)task fileTasks:(NSArray *)filetasks; 16 | //显示弹出框 17 | - (void)showAlertController:(UIAlertController *)ac; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Utils/LocalizedManager/SupportLanguages.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | language 6 | 7 | 简体中文 8 | 繁体中文 9 | English 10 | Tiếng Việt 11 | 12 | directory 13 | 14 | English 15 | en 16 | 中文简体 17 | zh-Hans 18 | 繁体中文 19 | zh-Hant 20 | Tiếng Việt 21 | vi 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Template/MDNS/MDNSManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // MDNSManager.h 3 | // AiHome 4 | // 5 | // Created by macmini on 2022/5/10. 6 | // 7 | 8 | #import 9 | 10 | @class WKWebView; 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface MDNSManager : NSObject 15 | 16 | - (instancetype)init NS_UNAVAILABLE; 17 | + (instancetype)new NS_UNAVAILABLE; 18 | 19 | + (instancetype)shared; 20 | 21 | // 这里有个意外的功能,会有UDP授权弹窗 22 | - (void)switchMDNS:(BOOL)onOrOff completionHandler:(void (^)(int code, NSError * _Nullable error))completionHandler; 23 | 24 | - (NSString *)IPForDeviceId:(NSString *)deviceId; 25 | - (int)portForDeviceId:(NSString *)deviceId; 26 | 27 | @end 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/DeviceNetworkManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceNetworkManager.h 3 | // 此类用来测试服务器是否可达 4 | 5 | #import 6 | 7 | typedef void(^ReachabilityResult)(BOOL isReachable); 8 | 9 | @interface DeviceNetworkManager : NSObject 10 | 11 | + (instancetype)sharedManager; 12 | 13 | - (void)deviceReachability:(ReachabilityResult)result; 14 | 15 | - (BOOL)cancel; 16 | 17 | @end 18 | 19 | FOUNDATION_EXTERN BOOL UsersCannotUseTheNetwork(NSInteger status, BOOL isLoadOnWiFi);// status is AFNetworkReachabilityStatus 20 | FOUNDATION_EXTERN NSNotificationName const NetworkUsableDidChangeNotification; 21 | FOUNDATION_EXTERN NSNotificationName const NetworkUsableItem; 22 | 23 | -------------------------------------------------------------------------------- /Development_ExportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | compileBitcode 6 | 7 | destination 8 | export 9 | method 10 | development 11 | signingStyle 12 | automatic 13 | stripSwiftSymbols 14 | 15 | teamID 16 | 1111111111 17 | thinning 18 | <none> 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/TransferCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // TransferCell.h 3 | // 4 | // 5 | // Created by mxm on 2018/4/24. 6 | // Copyright © 2018年 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class FileTask; 12 | 13 | @interface TransferCell : UITableViewCell 14 | 15 | /** 显示文件大小,或者传输速度 */ 16 | @property (nonatomic, strong) UILabel *byteLabel; 17 | /** 是否为正在传输cell */ 18 | @property (nonatomic, assign, getter=isProgressCell) BOOL progressCell; 19 | 20 | + (instancetype)dequeueReusableCellWithTableView:(UITableView *)tableView; 21 | 22 | - (void)setImageWithFileTask:(FileTask *)task; 23 | 24 | - (void)setFileTask:(FileTask *)task; 25 | 26 | - (void)setPausedStatus:(BOOL)isPaused; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /iOS_App_Template/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // iOS_App_Template 4 | // 5 | // Created by mxm on 2018/1/22. 6 | // Copyright © 2019 mxm. All rights reserved. 7 | // 8 | 9 | //https://www.jianshu.com/p/7f8472a97aa6 10 | //https://www.jianshu.com/p/ccd5691340fc 11 | //https://www.zybuluo.com/evolxb/note/482251 12 | //https://blog.csdn.net/u014228527/article/details/54092162 13 | //https://www.jianshu.com/p/9700ccae9f8e 这个关于推送非常的详细 14 | 15 | #import "ViewController.h" 16 | 17 | @interface ViewController () 18 | 19 | @end 20 | 21 | @implementation ViewController 22 | 23 | - (void)viewDidLoad { 24 | [super viewDidLoad]; 25 | // Do any additional setup after loading the view, typically from a nib. 26 | } 27 | 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/FileTask+WCTTableCoding.h: -------------------------------------------------------------------------------- 1 | // 2 | // FileTask+WCTTableCoding.h 3 | 4 | #import "FileTask.h" 5 | #import 6 | 7 | @interface FileTask (WCTTableCoding) 8 | 9 | WCDB_PROPERTY(Id) 10 | WCDB_PROPERTY(mac) 11 | WCDB_PROPERTY(userId) 12 | WCDB_PROPERTY(fileName) 13 | WCDB_PROPERTY(fileExt) 14 | WCDB_PROPERTY(mediaType) 15 | WCDB_PROPERTY(state) 16 | WCDB_PROPERTY(size) 17 | WCDB_PROPERTY(completedSize) 18 | WCDB_PROPERTY(type) 19 | WCDB_PROPERTY(serverPath) 20 | WCDB_PROPERTY(createTime) 21 | WCDB_PROPERTY(localPath) 22 | WCDB_PROPERTY(currentFragment) 23 | WCDB_PROPERTY(totalFragment) 24 | WCDB_PROPERTY(assetLocalIdentifier) 25 | WCDB_PROPERTY(filetype) 26 | WCDB_PROPERTY(resumeDataName) 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Utils/LocalizedManager/LocalizedManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedManager.h 3 | // 4 | // Created by mxm on 2018/5/7. 5 | // Copyright © 2018 mxm. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | #define LMLocalizedString(key, comment) \ 13 | [LMCurrentBundle() localizedStringForKey:(key) value:@"" table:nil] 14 | //#define LMLocalizedString(key, comment) \ 15 | //NSLocalizedStringFromTableInBundle(key, nil, LMCurrentBundle(), nil) 16 | 17 | @interface LocalizedManager : NSObject 18 | 19 | + (NSArray *)supportedLanguages; 20 | + (void)changeTo:(NSString *)language; 21 | + (NSUInteger)currentLanguageIndexInSupportedLanguages; 22 | 23 | @end 24 | 25 | FOUNDATION_EXTERN NSBundle * LMCurrentBundle(void); 26 | FOUNDATION_EXTERN NSNotificationName const ChangeLanguageNotification; 27 | //iOS系统设置语言时用的Change这个单词 28 | NS_ASSUME_NONNULL_END 29 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/User+WCTTableCoding.h: -------------------------------------------------------------------------------- 1 | // 2 | // User+WCTTableCoding.h 3 | 4 | #import "User.h" 5 | #import 6 | 7 | @interface User (WCTTableCoding) 8 | 9 | //使用WCDB_PROPERTY宏在头文件声明需要绑定到数据库表的字段 10 | WCDB_PROPERTY(Id) 11 | WCDB_PROPERTY(mac) 12 | WCDB_PROPERTY(account) 13 | WCDB_PROPERTY(password) 14 | WCDB_PROPERTY(lastLoginTime) 15 | WCDB_PROPERTY(rememberMe) 16 | WCDB_PROPERTY(loadOnWiFi) 17 | WCDB_PROPERTY(patternLockOn) 18 | WCDB_PROPERTY(patternPassword) 19 | WCDB_PROPERTY(secondsOfLockDelayed) 20 | WCDB_PROPERTY(numberOfRemainingDrawing) 21 | WCDB_PROPERTY(autoBackupAlbum) 22 | WCDB_PROPERTY(stopBackupAlbumWhenLowBattery) 23 | WCDB_PROPERTY(backupPhotos) 24 | WCDB_PROPERTY(backupVideos) 25 | WCDB_PROPERTY(totalBackup) 26 | WCDB_PROPERTY(completedBackup) 27 | WCDB_PROPERTY(isPauseAllDownload) 28 | WCDB_PROPERTY(isPauseAllUpload) 29 | WCDB_PROPERTY(notFirstAutoBackupAlbum) 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Utils/NSInputStreamSkip/NSInputStream+Skip.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSInputStream+Skip.m 3 | // SourceTest 4 | // 5 | // Created by min on 2019/10/20. 6 | // Copyright © 2019 min. All rights reserved. 7 | // 8 | 9 | #import "NSInputStream+Skip.h" 10 | 11 | @implementation NSInputStream (Skip) 12 | 13 | - (void)skip:(NSUInteger)byteCount 14 | { 15 | if (self.streamStatus == NSStreamStatusAtEnd 16 | || self.streamStatus == NSStreamStatusClosed 17 | || self.streamStatus == NSStreamStatusError) { 18 | return; 19 | } 20 | if (self.streamStatus == NSStreamStatusNotOpen) { 21 | [self open]; 22 | } 23 | NSUInteger const maxLen = MIN(byteCount, 16384);//16k 24 | uint8_t buffer[maxLen]; 25 | NSInteger size = 0; 26 | while (byteCount > 0) { 27 | size = [self read:buffer maxLength:MIN(byteCount, maxLen)]; 28 | if (size < 1) return; 29 | 30 | byteCount -= size; 31 | } 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/UIImage+Format.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Format.m 3 | 4 | #import "UIImage+Format.h" 5 | #import 6 | 7 | @implementation UIImage (Format) 8 | 9 | + (BOOL)isHEIF:(PHAsset *)asset 10 | { 11 | BOOL isHEIF = NO; 12 | if ([UIDevice currentDevice].systemVersion.floatValue > 9.0) { 13 | NSArray *resourceList = [PHAssetResource assetResourcesForAsset:asset]; 14 | NSString *UTI; 15 | for (PHAssetResource *resource in resourceList) { 16 | UTI = resource.uniformTypeIdentifier; 17 | if ([UTI isEqualToString:AVFileTypeHEIF] || [UTI isEqualToString:AVFileTypeHEIC]) { 18 | isHEIF = YES; 19 | break; 20 | } 21 | } 22 | } else { 23 | NSString *UTI = [asset valueForKey:@"uniformTypeIdentifier"]; 24 | isHEIF = [UTI isEqualToString:@"public.heif"] || [UTI isEqualToString:@"public.heic"]; 25 | } 26 | return isHEIF; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Utils/Utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.h 3 | // iOS_App_Template 4 | // 5 | // Created by mxm on 2021/7/12. 6 | // Copyright © 2021 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface Utils : NSObject 14 | 15 | // 计算文件MD5 16 | + (NSString *)md5OfFileAtPath:(NSString *)filePath; 17 | // 图片缩略图 18 | + (UIImage *)IOCompressImage:(NSData *)data size:(CGSize)size; 19 | // 获取文件夹大小 20 | + (unsigned long long)fetchDirSize:(NSString *)dirPath; 21 | // 获取可用存储空间大小 22 | + (uint64_t)availableSpace 23 | 24 | + (NSData *)encryptUseAES128CBC:(NSData *)data key:(Byte *)key; 25 | 26 | + (NSData *)decryptUseAES128CBC:(NSData *)data key:(Byte *)key; 27 | 28 | + (NSData *)encryptUseAES128ECB:(NSData *)data key:(Byte *)key; 29 | 30 | + (NSData *)decryptUseAES128ECB:(NSData *)data key:(Byte *)key; 31 | 32 | + (NSData *)encryptUseAES128CBCPKCS7:(NSData *)data key:(Byte *)key; 33 | 34 | + (NSData *)decryptUseAES128CBCPKCS7:(NSData *)data key:(Byte *)key; 35 | 36 | @end 37 | 38 | NS_ASSUME_NONNULL_END 39 | -------------------------------------------------------------------------------- /Utils/_XMAutoLaunch.m: -------------------------------------------------------------------------------- 1 | // 2 | // XmAutoLaunch 3 | // 随app启动自动运行一些代码。减少必须别人调用的麻烦。 4 | // 5 | 6 | #import 7 | 8 | @interface NSObject (XMAutoLaunch) 9 | 10 | @end 11 | 12 | @interface XMAutoLaunch : NSObject 13 | 14 | + (void)xm_applicationDidFinishLaunchingNotification:(NSNotification *)notification; 15 | 16 | @end 17 | 18 | @implementation NSObject (XMAutoLaunch) 19 | 20 | #pragma mark - load 21 | + (void)load 22 | { 23 | [[NSNotificationCenter defaultCenter] addObserver:[XMAutoLaunch class] selector:@selector(xm_applicationDidFinishLaunchingNotification:) name:UIApplicationDidFinishLaunchingNotification object:nil]; 24 | } 25 | 26 | @end 27 | 28 | @implementation XMAutoLaunch 29 | 30 | // 此方法会在调用 - application:(UIApplication *) didFinishLaunchingWithOptions:(NSDictionary *) 之后调用 31 | + (void)xm_applicationDidFinishLaunchingNotification:(NSNotification *)notification 32 | { 33 | // 防止NSNotificationCenter删除其他开发者添加的监听,这里自己新建了一个类。 34 | [[NSNotificationCenter defaultCenter] removeObserver:[XMAutoLaunch class]]; 35 | 36 | // 处理自己的一些逻辑 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Template/BLE/BLEUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLEUtils.h 3 | // 4 | // 5 | // Created by min on 2021/4/28. 6 | // 7 | 8 | #import 9 | 10 | /** 11 | @param code 结果码 12 | @param dataFromBle ble返回的结果 13 | @param msg 结果描述 14 | */ 15 | typedef void(^BLEConfigCompletion)(int code, NSDictionary * _Nullable dataFromBle, NSString * _Nullable msg); 16 | typedef void(^BLEScanResult)(int code, NSArray * _Nullable bles, NSString * _Nullable msg); 17 | 18 | NS_ASSUME_NONNULL_BEGIN 19 | 20 | @interface BLEUtils : NSObject 21 | 22 | - (instancetype)initWithFetchState:(void (^_Nullable)(NSInteger state))fetchState; 23 | /** 24 | 蓝牙状态: 25 | 0:已开启 26 | -1:未开启 27 | -2:没权限 28 | -3:不支持蓝牙 29 | -4:蓝牙重置中,即将更新 30 | -5 : 不知道的状态,即将更新 31 | */ 32 | - (NSInteger)bleState; 33 | //扫描后在 [didDiscoverBles: callbackId:] 34 | //- (void)scanDevice:(NSString *)callbackId; 35 | 36 | - (void)scanDevice:(BLEScanResult _Nullable)result;//result 是NSArray类型的 37 | 38 | - (void)sendMessage:(NSString *)message bleName:(NSString *)name completion:(BLEConfigCompletion _Nullable)completion;//result 是NSDictionary类型的 39 | //当不再等待蓝牙返回消息后,请调用此方法 40 | - (void)finish; 41 | 42 | @end 43 | 44 | NS_ASSUME_NONNULL_END 45 | -------------------------------------------------------------------------------- /Template/ShouldNotAutorotate/UIViewController+ShouldNotAutorotate.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+ShouldNotAutorotate.m 3 | 4 | #import "UIViewController+ShouldNotAutorotate.h" 5 | #import 6 | 7 | @implementation UIViewController (ShouldNotAutorotate) 8 | 9 | + (void)load 10 | { 11 | Method sa = class_getInstanceMethod(self, @selector(shouldAutorotate)); 12 | Method xm_sa = class_getInstanceMethod(self, @selector(xm_shouldAutorotate)); 13 | method_exchangeImplementations(sa, xm_sa); 14 | 15 | Method sio = class_getInstanceMethod(self, @selector(supportedInterfaceOrientations)); 16 | Method xm_sio = class_getInstanceMethod(self, @selector(xm_supportedInterfaceOrientations)); 17 | method_exchangeImplementations(sio, xm_sio); 18 | } 19 | 20 | - (BOOL)xm_shouldAutorotate 21 | { 22 | if ([self conformsToProtocol:@protocol(ShouldNotAutorotate)]) return NO; 23 | return [self xm_shouldAutorotate];//返回原来的结果 24 | } 25 | 26 | - (UIInterfaceOrientationMask)xm_supportedInterfaceOrientations 27 | { 28 | if ([self conformsToProtocol:@protocol(ShouldNotAutorotate)]) return UIInterfaceOrientationMaskPortrait; 29 | return [self xm_supportedInterfaceOrientations];//返回原来的结果 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /Template/Webview/URLSchemeHandler.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | // CDVURLSchemeHandler.m文件地址 https://github.com/apache/cordova-ios/tree/master/CordovaLib/Classes/Public 21 | // CDVWebViewEngine.m 文件中设置了 [config setURLSchemeHandler:urlSchemeHandler forURLScheme:@"xxx"]; 22 | 23 | #import 24 | #import 25 | 26 | 27 | @interface URLSchemeHandler : NSObject 28 | 29 | @property (nonatomic, strong) UIViewController *viewController; 30 | // 31 | //@property (nonatomic) CDVPlugin* schemePlugin; 32 | // 33 | //- (instancetype)initWithVC:(CDVViewController *)controller; 34 | 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /Utils/InputLimiter/InputLimiter.h: -------------------------------------------------------------------------------- 1 | // 2 | // InputLimiter.h 3 | // 4 | 5 | #import 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | @interface InputLimiter : NSObject 10 | 11 | /** 12 | 限制数字 13 | @param upperLimit 数字上限 14 | */ 15 | + (instancetype)limiterNumberTextField:(UITextField *)textField upperLimit:(int)upperLimit; 16 | 17 | /** 18 | 限制16进制数字 19 | @param upperLimit 数字上限 20 | */ 21 | + (instancetype)limiterHexadecimalNumberTextField:(UITextField *)textField upperLimit:(int)upperLimit; 22 | 23 | /** 24 | 限制整数 25 | @param upperLimit 限制正数上限 26 | @param lowerLimit 限制负数下限 27 | */ 28 | + (instancetype)limiterIntegerTextField:(UITextField *)textField positiveUpperLimit:(int)upperLimit negativeLowerLimit:(int)lowerLimit; 29 | 30 | /** 31 | 限制正小数 32 | @param digits 限制小数位数 33 | @param upperLimit 限制正小数上限 34 | */ 35 | + (instancetype)limiterFloatTextField:(UITextField *)textField fractionDigits:(UInt8)digits upperLimit:(float)upperLimit; 36 | 37 | /** 38 | 限制有理数 39 | @param digits 限制小数位数 40 | @param upperLimit 限制正数上限 41 | @param lowerLimit 限制负数下限 42 | */ 43 | + (instancetype)limiterRationalNumberTextField:(UITextField *)textField fractionDigits:(UInt8)digits positiveUpperLimit:(float)upperLimit negativeLowerLimit:(float)lowerLimit; 44 | 45 | @end 46 | 47 | @interface UITextField (InputLimiterProperty) 48 | 49 | @property (nonatomic, strong) InputLimiter *inputLimiter; 50 | 51 | @end 52 | 53 | NS_ASSUME_NONNULL_END 54 | -------------------------------------------------------------------------------- /Template/Backup/BackupManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // BackupManager.h 3 | // 依赖DownloadUploadBackupCommon中的部分文件 4 | /** 5 | 断点上传的思路: 6 | 上传前先查询服务器当前文件是否上传过,如果上传过,返回已上传的字节数, 7 | 然后接着已上传的继续上传,否则从0开始上传; 8 | 第一次上传时给服务器,除文件名外,最好还提交一个文件计算过的唯一标识符(如 9 | MD5),以后断点续传时,可检查上次上传的文件和此次的是否为同一个文件, 10 | 若不是,就要相应的做出处理,否则,继续断点续传 11 | 12 | 提示:NSMutableURLRequest可以设置NSInputStream作为HTTPBodyStream 13 | 14 | 我下面的是用的分片上传,然后本地记录已上传的片段数,这种方式不太好 15 | */ 16 | 17 | #import 18 | 19 | FOUNDATION_EXTERN NSNotificationName const BackupFileCountUpdateNotification; 20 | 21 | @interface BackupManager : NSObject 22 | /** 用户, 不能为空 */ 23 | //@property (nonatomic, weak) User *user; 24 | /** 是否正在备份 */ 25 | @property (nonatomic, assign, readonly) BOOL isInProgress; 26 | /** 备份文件总数 */ 27 | @property (nonatomic, assign, readonly) NSUInteger total; 28 | /** 已经备份了多少个文件 */ 29 | @property (nonatomic, assign, readonly) NSUInteger completedCount; 30 | 31 | /** 32 | 单例类 33 | 在调用前请设置好 User.currentUser 34 | @return BackupManager单例 35 | */ 36 | + (instancetype)shareManager; 37 | 38 | + (instancetype)new NS_UNAVAILABLE;//不可调用 39 | - (instancetype)init NS_UNAVAILABLE;//不可调用 40 | 41 | /** 42 | 立刻备份 43 | 先调用[BackupManager switchAutoBackup:YES]才会有效 44 | */ 45 | - (void)backupImmediately; 46 | 47 | /** 48 | 自动备份开关 49 | 50 | @param isAuto YES:开,NO:关 51 | */ 52 | - (void)switchAutoBackup:(BOOL)isAuto; 53 | 54 | /** 55 | 停止备份,关闭自动备份时调用,重新选择备份相册时也要调用 56 | */ 57 | - (void)stopBackup; 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /iOS_App_Template/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /iOS_App_Template/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Utils/Const.h: -------------------------------------------------------------------------------- 1 | // 2 | // Const.h 3 | 4 | #ifndef Const_h 5 | #define Const_h 6 | 7 | #pragma mark - 空判断 8 | NS_INLINE 9 | BOOL IsEmptyString(NSString *string) { 10 | return nil == string || (id)kCFNull == string || string.length == 0; 11 | } 12 | 13 | NS_INLINE 14 | BOOL IsTextEmptyString(NSString *string) { 15 | return nil == string || string.length == 0; 16 | } 17 | 18 | NS_INLINE 19 | BOOL IsEmptyObj(id obj) { 20 | return nil == obj || (id)kCFNull == obj; 21 | } 22 | 23 | //调用C语言的API来获得文件的MIMEType 24 | NS_INLINE 25 | NSString * mimeTypeForFileAtPath(NSString *path) 26 | { 27 | NSString *ext = [path pathExtension]; 28 | if (IsTextEmptyString(ext)) { 29 | return nil; 30 | } 31 | //此函数需要引入 #import 32 | // [path pathExtension] 获得文件的后缀名 MIME类型字符串转化为UTI字符串 33 | CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)ext, NULL); 34 | // UTI字符串转化为后缀扩展名 35 | CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType); 36 | CFRelease(UTI); 37 | // application/octet-stream,此参数表示通用的二进制类型。 38 | if (!MIMEType) { 39 | return @"application/octet-stream"; 40 | } 41 | //iOS14起的MIMEType获取方式,还得引入: #import 42 | // [UTType typeWithFilenameExtension:ext].preferredMIMEType] 43 | return (__bridge NSString *)(MIMEType); 44 | } 45 | 46 | #ifdef DEBUG //调试阶段 47 | #define NSLog(...) printf("第%d行 %s\n%s\n\n", __LINE__, __func__, [NSString stringWithFormat:__VA_ARGS__].UTF8String) 48 | #else //发布 49 | #define NSLog(...) 50 | #endif 51 | 52 | #define NSStringFromeBOOL(x) (x) ? @"YES" : @"NO" 53 | 54 | #endif /* Const_h */ 55 | -------------------------------------------------------------------------------- /iOS_App_Template/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Utils/XMLock.h: -------------------------------------------------------------------------------- 1 | // 2 | // XMLock.h 3 | // XMLock 4 | // 5 | // Created by mxm on 2018/7/20. 6 | // Copyright © 2018年 mxm. All rights reserved. 7 | // 8 | // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-SW1 9 | // https://blog.csdn.net/iosswift/article/details/44597759 10 | 11 | /* 12 | NSCache,NSUserDefaults是线程安全的,不需要使用 13 | 14 | NSMutableArray 15 | NSMutableSet 16 | NSMutableOrderedSet 17 | NSCountedSet 18 | NSMutableIndexSet 19 | NSMutableDictionary 20 | 21 | NSMapTable 22 | NSHashTable 23 | NSPointerArray 24 | 25 | NSAutoreleasePool 26 | //这个不知道是否线程安全 27 | NSUbiquitousKeyValueStore 28 | 29 | --------------使用--------------- 30 | //创建锁 31 | static XMLock lock;//保证某个作用域内唯一 32 | static dispatch_once_t onceToken; 33 | dispatch_once(&onceToken, ^{ 34 | lock = XM_CreateLock(); 35 | }); 36 | 37 | 方式一:注意别嵌套成死锁 38 | XM_Lock(lock); 39 | ... //中间不要有return、break、continue、throw、goto...等中断语句,否则XM_UnLock不能被执行 40 | XM_UnLock(lock); 41 | 42 | 43 | 方式二:注意别嵌套成死锁 44 | id obj; 45 | //推荐用法 46 | XM_OnThreadSecure(lock, [arr addObject:obj]); 47 | XM_OnThreadSecure(lock, [arr removeObject:obj]); 48 | 49 | //不推荐这么使用,影响阅读,可以使用方式一 50 | XM_OnThreadSecure( 51 | lock, 52 | int v = arr.count; 53 | [arr addObject:obj]; 54 | sleep(3);//这么使用可以测试锁是否有效 55 | ); 56 | NSLog(@"%d", v); 57 | */ 58 | 59 | #ifndef XMLock_h 60 | #define XMLock_h 61 | 62 | #include 63 | 64 | #define XM_CreateLock() dispatch_semaphore_create(1) 65 | #define XM_Lock(x) dispatch_semaphore_wait(x, DISPATCH_TIME_FOREVER) 66 | #define XM_UnLock(x) dispatch_semaphore_signal(x) 67 | 68 | typedef dispatch_semaphore_t XMLock; 69 | 70 | /** 71 | 简单包裹 72 | @param lock XMLock 73 | @param x 任意表达式,表达式中不要有return、break、continue、throw、goto...等中断语句,否则XM_UnLock不能被执行 74 | */ 75 | #define XM_OnThreadSafe(lock, x) \ 76 | XM_Lock(lock); \ 77 | x; \ 78 | XM_UnLock(lock) 79 | //__VA_ARGS__ 80 | 81 | #endif /* XMLock_h */ 82 | -------------------------------------------------------------------------------- /iOS_App_Template/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Template/LaunchViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // iOS_App_Template 4 | // 5 | // Created by mxm on 2018/1/22. 6 | // Copyright © 2019 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface LaunchViewController : UIViewController 12 | { 13 | UIImageView *LaunchScreen; 14 | } 15 | 16 | @end 17 | 18 | @implementation LaunchViewController 19 | 20 | - (void)viewDidLoad { 21 | [super viewDidLoad]; 22 | 23 | [self addLaunchScreen]; 24 | 25 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 26 | [self removeLaunchScreen]; 27 | }); 28 | } 29 | 30 | /** 31 | 动态替换启动页 32 | https://blog.csdn.net/olsQ93038o99S/article/details/108924170 33 | https://github.com/iversonxh/DynamicLaunchImage 34 | 添加一个和app启动页一模一样的页面,让用户误以为延长了启动页,此页面可做广告页 35 | */ 36 | - (void)addLaunchScreen 37 | { 38 | UIViewController *vc = [[UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil] instantiateInitialViewController]; 39 | vc.view.frame = UIScreen.mainScreen.bounds; 40 | // [vc.view setNeedsLayout]; 41 | // [vc.view layoutIfNeeded]; 42 | // [vc viewWillLayoutSubviews]; 43 | // [vc viewDidLayoutSubviews]; 44 | // [vc viewWillAppear:YES]; 45 | // [vc viewDidAppear:YES]; 46 | // [self.view addSubview:vc.view]; 47 | UIGraphicsBeginImageContextWithOptions(UIScreen.mainScreen.bounds.size, NO, UIScreen.mainScreen.scale); 48 | [vc.view.layer renderInContext:UIGraphicsGetCurrentContext()]; 49 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext();//最好缓存到文档目录下,免除每次制作 50 | UIGraphicsEndImageContext(); 51 | LaunchScreen = [[UIImageView alloc] initWithImage:image]; 52 | LaunchScreen.frame = UIScreen.mainScreen.bounds; 53 | [self.view addSubview:LaunchScreen]; 54 | } 55 | 56 | - (void)removeLaunchScreen 57 | { 58 | [UIView animateWithDuration:0.3 animations:^{ 59 | self->LaunchScreen.alpha = 0; 60 | } completion:^(BOOL finished) { 61 | [self->LaunchScreen removeFromSuperview]; 62 | }]; 63 | } 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /Template/Test2ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // iOS_App_Template 4 | // 5 | // Created by min on 2020/6/13. 6 | // Copyright © 2020 min. All rights reserved. 7 | // 8 | // 详细请看下面的注释 9 | 10 | import UIKit 11 | 12 | class Test2ViewController: UIViewController { 13 | let a = 123 14 | 15 | // convenience init() {//便利构造器 16 | // self.init()//此方法可能是从ObjC的UIKit bridge(桥接)过来的。 17 | // self.init(nibName: nil, bundle: nil)//用这个是不是会好点? 18 | // //..... 19 | // } 20 | /** 21 | https://swiftgg.gitbook.io/swift/swift-jiao-cheng/14_initialization#class-inheritance-and-initialization 22 | 构造器的自动继承 23 | 子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。事实上,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。 24 | 假设你为子类中引入的所有新属性都提供了默认值,以下 2 个规则将适用: 25 | 规则 1 26 | 如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。 27 | 规则 2 28 | 如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。 29 | 即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。 30 | 注意 31 | 子类可以将父类的指定构造器实现为便利构造器来满足规则 2。 32 | 33 | 类类型的构造器代理 34 | 为了简化指定构造器和便利构造器之间的调用关系,Swift 构造器之间的代理调用遵循以下三条规则: 35 | 规则 1 36 | 指定构造器必须调用其直接父类的的指定构造器。 37 | 规则 2 38 | 便利构造器必须调用同类中定义的其它构造器。 39 | 规则 3 40 | 便利构造器最后必须调用指定构造器。 41 | 42 | 两段式构造过程:请自己google 43 | */ 44 | // init() { 45 | //// super.init()//根据上面👆规则,UIViewController没有继承init()方法,所以不能直接调用,而指定构造器必须调用其直接父类的的指定构造器。 46 | //// init(name: "123")//指定构造器也不能调用自己其他的指定构造器 47 | // super.init(nibName: nil, bundle: nil) 48 | //// init(name: "123") 49 | // } 50 | // required init?(coder: NSCoder) {//此方法来自NSCoding,可百度查询为什么必须重写 51 | // fatalError("init(coder:) has not been implemented") 52 | // } 53 | // init(name:String) { 54 | // super.init(nibName: nil, bundle: nil) 55 | //// init()//指定构造器也不能调用自己其他的指定构造器 56 | // } 57 | 58 | 59 | override func viewDidLoad() { 60 | super.viewDidLoad() 61 | let vc = Test2ViewController();//根据规则,父类没有继承init(),为啥还能用?原因可能是这个初始化方式是来自UIKit,也就是调用了ObjC下的UIViewController初始化方法,是ObjC bridge(桥接)过来的。 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Template/Download/DownloadManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadManager.h 3 | 4 | #import 5 | 6 | @class DownloadManager, FileTask, RequestModel; 7 | 8 | @protocol DownloadManagerDelegate 9 | @optional 10 | 11 | - (void)downloadManager:(DownloadManager *)manager didAddNewFileTasks:(NSArray *)fileTasks; 12 | 13 | - (void)downloadManager:(DownloadManager *)manager didChangeFileTask:(FileTask *)fileTask; 14 | //将要移动 15 | - (void)downloadManager:(DownloadManager *)manager willMoveFileTaskToArr:(NSArray *)toArr; 16 | //移动了 17 | - (void)downloadManager:(DownloadManager *)manager didMoveFileTask:(FileTask *)fileTask fromArr:(NSArray *)fromArr fromIndex:(NSUInteger)fromIdx toArr:(NSArray *)toArr toIdx:(NSUInteger)toIdx; 18 | //没有正在下载的任务 19 | - (void)downloadManager:(DownloadManager *)manager noTasksBeingDownloaded:(BOOL)isNo; 20 | @end 21 | 22 | @interface DownloadManager : NSObject 23 | /** 代理 */ 24 | @property (nonatomic, weak) id delegate; 25 | /** 正在下载的任务,为了减少拷贝,用的strong */ 26 | @property (nonatomic, strong, readonly) NSArray *downloadingTasks; 27 | /** 下载成功的任务 */ 28 | @property (nonatomic, strong, readonly) NSArray *successTasks; 29 | /** 下载失败的任务 */ 30 | @property (nonatomic, strong, readonly) NSArray *failureTasks; 31 | 32 | + (instancetype)shareManager; 33 | 34 | /** 35 | 下载文件 36 | 37 | @param model 文件 38 | @param serverDirectory 文件所在目录 39 | */ 40 | - (void)downloadFile:(RequestModel *)model fromserverDirectory:(NSString *)serverDirectory; 41 | 42 | /** 43 | 暂停所有 44 | 45 | @return YES表示:执行完毕,不会回调noTasksBeingDownloaded;NO表示:未执行完毕,会回调noTasksBeingDownloaded 46 | */ 47 | - (BOOL)pauseAll; 48 | 49 | /** 50 | 暂停单个FileTask 51 | 52 | @param ftask FileTask 53 | */ 54 | - (void)pauseFileTask:(FileTask *)ftask; 55 | 56 | /** 57 | 恢复所有 58 | 59 | @return YES表示:执行完毕,不会回调noTasksBeingDownloaded;NO表示:未执行完毕,会回调noTasksBeingDownloaded 60 | */ 61 | - (BOOL)resumeAll; 62 | 63 | /** 64 | 恢复单个FileTask 65 | 66 | @param ftask FileTask 67 | */ 68 | - (void)resumeFileTask:(FileTask *)ftask; 69 | 70 | /** 71 | 删除所有FileTask 72 | */ 73 | - (void)deleteAll; 74 | 75 | /** 76 | 删除单个FileTask 77 | 78 | @param ftask FileTask 79 | */ 80 | - (void)deleteFileTask:(FileTask *)ftask; 81 | 82 | /** 83 | 删除所有选中的FileTask 84 | */ 85 | - (void)deleteAllSelected; 86 | 87 | - (void)selectAllTasks; 88 | 89 | - (void)deselectAllTasks; 90 | 91 | - (BOOL)isAllPaused; 92 | 93 | - (void)redownload:(FileTask *)fileTask; 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /Template/Upload/UploadManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // UploadManager.h 3 | /** 4 | 断点上传的思路: 5 | 上传前先查询服务器当前文件是否上传过,如果上传过,返回已上传的字节数, 6 | 然后接着已上传的继续上传,否则从0开始上传; 7 | 第一次上传时给服务器,除文件名外,最好还提交一个文件计算过的唯一标识符(如 8 | MD5),以后断点续传时,可检查上次上传的文件和此次的是否为同一个文件, 9 | 若不是,就要相应的做出处理,否则,继续断点续传 10 | 11 | 提示:NSMutableURLRequest可以设置NSInputStream作为HTTPBodyStream 12 | 13 | 我下面的是用的分片上传,然后本地记录已上传的片段数,这种方式不太好 14 | */ 15 | 16 | #import 17 | 18 | @class User, FileTask, UploadManager, PHAsset; 19 | 20 | @protocol UploadManagerDelegate 21 | 22 | @optional 23 | //fileTasks可能是uploadingTasks或者failureTasks 24 | - (void)uploadManager:(UploadManager *)manager didAddNewFileTasks:(NSArray *)fileTasks; 25 | 26 | - (void)uploadManager:(UploadManager *)manager didChangeFileTask:(FileTask *)fileTask; 27 | //将要移动 28 | - (void)uploadManager:(UploadManager *)manager willMoveFileTaskToArr:(NSArray *)toArr; 29 | //移动了 30 | - (void)uploadManager:(UploadManager *)manager didMoveFileTask:(FileTask *)fileTask fromArr:(NSArray *)fromArr fromIndex:(NSUInteger)fromIdx toArr:(NSArray *)toArr toIdx:(NSUInteger)toIdx; 31 | @end 32 | 33 | @interface UploadManager : NSObject 34 | 35 | /** 代理 */ 36 | @property (nonatomic, weak) id delegate; 37 | /** 上传任务 */ 38 | @property (nonatomic, strong, readonly) NSArray *uploadingTasks; 39 | /** 上传成功的任务 */ 40 | @property (nonatomic, strong, readonly) NSArray *successTasks; 41 | /** 上传失败的任务 */ 42 | @property (nonatomic, strong, readonly) NSArray *failureTasks; 43 | 44 | + (instancetype)shareManager; 45 | 46 | /** 47 | 上传相册文件 48 | 49 | @param assets PHAssets 50 | @param directory 目标目录 51 | @return 是否已添加到上传列表 52 | */ 53 | - (BOOL)uploadPHAsset:(NSArray *)assets toserverDirectory:(NSString *)directory; 54 | 55 | /** 56 | 暂停所有 57 | */ 58 | //- (void)pauseAll; 59 | 60 | /** 61 | 暂停单个FileTask 62 | 63 | @param ftask FileTask 64 | */ 65 | //- (void)pauseFileTask:(FileTask *)ftask; 66 | 67 | /** 68 | 恢复所有 69 | */ 70 | //- (void)resumeAll; 71 | 72 | /** 73 | 恢复单个FileTask 74 | 75 | @param ftask FileTask 76 | */ 77 | //- (void)resumeFileTask:(FileTask *)ftask; 78 | 79 | /** 80 | 删除所有FileTask 81 | */ 82 | - (void)deleteAll; 83 | 84 | /** 85 | 删除单个FileTask 86 | 87 | @param ftask FileTask 88 | */ 89 | - (void)deleteFileTask:(FileTask *)ftask; 90 | 91 | /** 92 | 删除所有选中的FileTask 93 | */ 94 | - (void)deleteAllSelected; 95 | 96 | - (void)selectAllTasks; 97 | 98 | - (void)deselectAllTasks; 99 | 100 | - (BOOL)reupload:(FileTask *)filetask; 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /Template/TableViewCellAutoCalculate.m: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCellAutoCalculate.m 3 | // 4 | // 5 | // Created by mxm on 2018/7/17. 6 | // Copyright © 2016 mxm. All rights reserved. 7 | // cell高度自适应 8 | 9 | #import 10 | 11 | @interface TableViewCellAutoCalculate : UIViewController 12 | { 13 | UITableView *_tableView; 14 | } 15 | 16 | @end 17 | 18 | @implementation TableViewCellAutoCalculate 19 | 20 | - (void)viewDidLoad 21 | { 22 | [super viewDidLoad]; 23 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain]; 24 | _tableView.frame = self.view.bounds; 25 | _tableView.delegate = self; 26 | _tableView.dataSource = self; 27 | _tableView.estimatedRowHeight = 44; //这样设置可以让cell去自适应内容的高度, 配合autolayout,也能自适应高度;不宜过大或者过小,最好是一个大概的平均值 28 | // tableView.rowHeight = UITableViewAutomaticDimension; 29 | [self.view addSubview:_tableView]; 30 | } 31 | 32 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 33 | { 34 | return 5; 35 | } 36 | 37 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 38 | { 39 | static NSString *identifier = @""; 40 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; 41 | if (nil == cell) { 42 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; 43 | cell.textLabel.numberOfLines = 0; 44 | } 45 | 46 | NSMutableString *txt = [NSMutableString stringWithString:@"沙悟净尬舞我;埃里克是点击发;历史的积分"]; 47 | for (int i = 0; i < indexPath.row; ++i) { 48 | [txt appendString:@"沙悟净尬舞我;埃里克是点击发;历史的积分爱上"]; 49 | } 50 | cell.textLabel.text = txt; 51 | 52 | //要视图自适应内容 53 | // [cell.contentView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize]; 54 | 55 | return cell; 56 | } 57 | 58 | - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath 59 | { 60 | return 44.0;//过大过小都不行 61 | } 62 | 63 | - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath 64 | { 65 | return UITableViewCellEditingStyleDelete; 66 | } 67 | 68 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath 69 | {//若不在当前方法线程中执行delete操作时,tableView的cell会有跳动bug,这个应该是系统bug,是autolayout产生的bug,固定cell的高度,不会有bug,esimatedRowHeight过大或者过小都有bug 70 | 71 | dispatch_async(dispatch_get_main_queue(), ^(void) {//测试tableView跳动bug 72 | [_tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 73 | }); 74 | } 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /Template/SpeechUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // SpeechUtils.m 3 | // iOS_App_Template 4 | // 5 | // Created by mac on 2021/1/6. 6 | // Copyright © 2022 mxm. All rights reserved. 7 | // 8 | 9 | #import "SpeechUtils.h" 10 | #import 11 | 12 | @interface SpeechUtils (Delegate) 13 | 14 | @end 15 | 16 | //要在 TARGETS -> General 加入 AVFoundation 依赖库 17 | @implementation SpeechUtils 18 | { 19 | AVSpeechSynthesizer *_avSpeaker; 20 | } 21 | 22 | - (void)player 23 | { 24 | //初始化语音合成器 25 | _avSpeaker = [[AVSpeechSynthesizer alloc] init]; 26 | _avSpeaker.delegate = self; 27 | //初始化要说出的内容 28 | AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:@"6 6 6"]; 29 | //设置语速,语速介于AVSpeechUtteranceMaximumSpeechRate和AVSpeechUtteranceMinimumSpeechRate之间 30 | //AVSpeechUtteranceMaximumSpeechRate 31 | //AVSpeechUtteranceMinimumSpeechRate 32 | //AVSpeechUtteranceDefaultSpeechRate 33 | utterance.rate = 0.5; 34 | 35 | //设置音高,[0.5 - 2] 默认 = 1 36 | //AVSpeechUtteranceMaximumSpeechRate 37 | //AVSpeechUtteranceMinimumSpeechRate 38 | //AVSpeechUtteranceDefaultSpeechRate 39 | utterance.pitchMultiplier = 1; 40 | 41 | //设置音量,[0-1] 默认 = 1 42 | utterance.volume = 1; 43 | 44 | //读一段前的停顿时间 45 | utterance.preUtteranceDelay = 1; 46 | //读完一段后的停顿时间 47 | utterance.postUtteranceDelay = 1; 48 | 49 | //设置声音,是AVSpeechSynthesisVoice对象 50 | //AVSpeechSynthesisVoice定义了一系列的声音, 主要是不同的语言和地区. 51 | //voiceWithLanguage: 根据制定的语言, 获得一个声音. 52 | //speechVoices: 获得当前设备支持的声音 53 | //currentLanguageCode: 获得当前声音的语言字符串, 比如”ZH-cn” 54 | //language: 获得当前的语言 55 | //通过特定的语言获得声音 56 | AVSpeechSynthesisVoice *voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"]; 57 | //通过voicce标示获得声音 58 | //AVSpeechSynthesisVoice *voice = [AVSpeechSynthesisVoice voiceWithIdentifier:AVSpeechSynthesisVoiceIdentifierAlex]; 59 | utterance.voice = voice; 60 | //开始朗读 61 | [_avSpeaker speakUtterance:utterance]; 62 | } 63 | 64 | #pragma mark - AVSpeechSynthesizerDelegate 65 | //已经开始 66 | - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didStartSpeechUtterance:(AVSpeechUtterance *)utterance 67 | { 68 | } 69 | //已经说完 70 | - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance 71 | { 72 | _avSpeaker = nil; 73 | //如果朗读要循环朗读,可以在这里再次调用朗读方法 74 | //[_avSpeaker speakUtterance:utterance]; 75 | } 76 | //已经暂停 77 | - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didPauseSpeechUtterance:(AVSpeechUtterance *)utterance 78 | { 79 | } 80 | //已经继续说话 81 | - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didContinueSpeechUtterance:(AVSpeechUtterance *)utterance 82 | { 83 | } 84 | //已经取消说话 85 | - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didCancelSpeechUtterance:(AVSpeechUtterance *)utterance 86 | { 87 | } 88 | //将要说某段话 89 | - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer willSpeakRangeOfSpeechString:(NSRange)characterRange utterance:(AVSpeechUtterance *)utterance 90 | { 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /Template/SelectFileViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // iOS_App_Template 4 | // 5 | // Created by mxm on 2023/10/1. 6 | // Copyright © 2023 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @import UniformTypeIdentifiers; 12 | 13 | @interface UIViewController (SelectFile) 14 | 15 | @end 16 | 17 | @implementation UIViewController (SelectFile) 18 | 19 | - (void)selectFile 20 | { 21 | UIDocumentPickerViewController *vc; 22 | if (@available(iOS 14.0, *)) { 23 | vc = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[ 24 | [UTType typeWithFilenameExtension:@"zip"], 25 | [UTType typeWithFilenameExtension:@"txt"] 26 | ]]; 27 | } else { 28 | // 官方类型:https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html#//apple_ref/doc/uid/TP40009259-SW1 29 | // 如果类型找不到或不知道怎么填,可以在高版本上这么打印: 30 | // NSLog(@"%@", [UTType typeWithFilenameExtension:@"apk"]); 31 | // 根据后缀打印得到的就是类型 32 | vc = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[ 33 | @"public.data", 34 | @"public.text"] inMode:UIDocumentPickerModeImport]; 35 | } 36 | vc.delegate = self; 37 | if (@available(iOS 13.0, *)) { 38 | vc.shouldShowFileExtensions = YES; 39 | } 40 | [self presentViewController:vc animated:YES completion:nil]; 41 | } 42 | 43 | - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls 44 | { 45 | [controller dismissViewControllerAnimated:YES completion:NULL]; 46 | NSURL *url = urls.firstObject; 47 | BOOL fileUrlAuthozied = [url startAccessingSecurityScopedResource]; 48 | if (!fileUrlAuthozied) { 49 | // 授权失败 50 | return; 51 | } 52 | 53 | // 通过文件协调工具来得到新的文件地址,以此得到文件保护功能 54 | NSFileCoordinator *fileCoordinator = [NSFileCoordinator new]; 55 | NSError *error; 56 | __weak __typeof(self) weakSelf = self; 57 | [fileCoordinator coordinateReadingItemAtURL:url options:kNilOptions error:&error byAccessor:^(NSURL *newURL) { 58 | __strong __typeof(weakSelf) self = weakSelf; 59 | if (nil == self) return; 60 | 61 | // 把 newURL 传递给你要用到的地方,注意,不能使用 newURL.absoluteString 获取文件,必须用newURL获取文件,否则获取不到文件 62 | // 为防止 newURL 无法访问,最好先拷贝一份文件到自己的沙盒下 63 | NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:newURL.lastPathComponent]; 64 | [NSFileManager.defaultManager removeItemAtPath:path error:NULL]; 65 | NSURL *url = [NSURL fileURLWithPath:path]; 66 | [NSFileManager.defaultManager copyItemAtURL:newURL toURL:url error:NULL]; 67 | 68 | // 获取文件信息 69 | NSDictionary *fileAttr = [NSFileManager.defaultManager attributesOfItemAtPath:url.path error:nil]; 70 | // 文件大小 71 | NSUInteger fileSize = [fileAttr fileSize]; 72 | // 文件流 73 | [NSInputStream inputStreamWithURL:url]; 74 | }]; 75 | [url stopAccessingSecurityScopedResource]; 76 | } 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /Template/LocalAuthentication.m: -------------------------------------------------------------------------------- 1 | // 2 | // LocalAuthentication.m 3 | // 4 | // 5 | // Created by mxm on 2019/2/10. 6 | // Copyright © 2019 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface LocalAuthentication : NSObject 12 | @end 13 | 14 | @implementation LocalAuthentication 15 | 16 | - (void)LocalAuthentication 17 | { 18 | /* 19 | iOS11在info.plist加入 20 | NSFaceIDUsageDescription 21 | 面容ID用于保证您的数据安全 22 | */ 23 | LAContext *context = [LAContext new]; 24 | context.localizedFallbackTitle = @""; // 隐藏左边的按钮(默认是忘记密码的按钮) 25 | //验证方式 26 | //LAPolicyDeviceOwnerAuthenticationWithBiometrics(只有指纹/FaceID验证功能) 27 | //LAPolicyDeviceOwnerAuthentication(包含指纹/FaceID验证和密码验证) 28 | NSError *error = nil; 29 | BOOL isSupport = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&error];// 30 | 31 | if (!isSupport) { 32 | NSLog(@"当前设备不支持TouchID"); 33 | return; 34 | } 35 | 36 | //起调身份验证 37 | [context evaluatePolicy:LAPolicyDeviceOwnerAuthentication localizedReason:@"我们需要验证你的身份" reply:^(BOOL success, NSError * _Nullable error) { 38 | 39 | if (success) { 40 | NSLog(@"身份 验证成功"); 41 | //biometryType在这里才会有值 42 | switch (context.biometryType) { 43 | case LABiometryNone: 44 | break; 45 | case LABiometryTypeTouchID: 46 | break; 47 | case LABiometryTypeFaceID: 48 | break; 49 | default: 50 | break; 51 | } 52 | return; 53 | } 54 | if (nil == error) { 55 | return; 56 | } 57 | 58 | switch (error.code) { 59 | case LAErrorAuthenticationFailed: 60 | NSLog(@"身份 验证失败"); 61 | break; 62 | case LAErrorUserCancel: 63 | NSLog(@"身份验证 被用户手动取消"); 64 | break; 65 | case LAErrorUserFallback: 66 | NSLog(@"用户不使用生物识别, 选择手动输入密码"); 67 | break; 68 | case LAErrorSystemCancel: 69 | NSLog(@"生物识别 被系统取消 (如遇到来电,锁屏,按了Home键等)"); 70 | break; 71 | case LAErrorPasscodeNotSet: 72 | NSLog(@"生物识别 无法启动,因为用户没有设置密码"); 73 | break; 74 | case LAErrorAppCancel: 75 | NSLog(@"当前软件被挂起并取消了授权 (如App进入了后台等)"); 76 | break; 77 | case LAErrorInvalidContext: 78 | NSLog(@"当前软件被挂起并取消了授权 (LAContext对象无效)"); 79 | break; 80 | case LAErrorBiometryNotAvailable: 81 | NSLog(@"生物识别 无效"); 82 | break; 83 | case LAErrorBiometryNotEnrolled: 84 | NSLog(@"生物识别 无法启动,因为用户没有设置 生物识别"); 85 | break; 86 | case LAErrorBiometryLockout: 87 | NSLog(@"生物识别 被锁定(连续多次验证TouchID失败,系统需要用户手动输入密码)"); 88 | break; 89 | case LAErrorNotInteractive:break; 90 | default: 91 | break; 92 | } 93 | }]; 94 | } 95 | 96 | @end 97 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/User.h: -------------------------------------------------------------------------------- 1 | // 2 | // User.h 3 | // 使用的是WCDB框架 4 | 5 | #import 6 | //#import 7 | 8 | FOUNDATION_EXTERN float const LowBatteryValue; 9 | FOUNDATION_EXTERN float const LowBatteryMustStopValue; 10 | //用户退出登录通知 11 | FOUNDATION_EXTERN NSNotificationName const UserLogoutNotification; 12 | FOUNDATION_EXTERN NSNotificationName const LoadOnWiFiSwitchNotification; 13 | 14 | /** 15 | 用户,用于保存用户的账号和设置等信息 16 | */ 17 | @interface User : NSObject /// 18 | /** 当前登录的用户 */ 19 | @property (nonatomic, strong, class) User *currentUser; 20 | 21 | #pragma mark - 账号 22 | /** 主键 */ 23 | @property (nonatomic, assign, readonly) int Id; 24 | /** 用户所属设备的Mac地址 */ 25 | @property (nonatomic, copy) NSString *mac; 26 | /** 认证码 */ 27 | //@property (nonatomic, copy) NSString *deviceAuthorization; 28 | /** 账号 */ 29 | @property (nonatomic, copy) NSString *account; 30 | /** 密码 */ 31 | @property (nonatomic, copy) NSString *password; 32 | /** 最后一次登录时间 */ 33 | @property (nonatomic, assign) NSTimeInterval lastLoginTime; 34 | /** 是否记住账号密码 */ 35 | @property (nonatomic, assign) BOOL rememberMe; 36 | /** 网络请求时需要的sessionId */ 37 | @property (nonatomic, copy) NSString *sessionId;//token?,不用保存到数据库? 38 | 39 | #pragma mark - 设置 40 | /** WiFi连接时上传/下载,默认YES */ 41 | @property (nonatomic, assign) BOOL loadOnWiFi;//transmission? Transmit? 42 | 43 | /** 手势密码锁是否开启 */ 44 | @property (nonatomic, assign) BOOL patternLockOn; 45 | /** 手势密码 */ 46 | @property (nonatomic, assign) NSInteger patternPassword; 47 | /** 手势密码锁延迟上锁时间,单位秒 */ 48 | @property (nonatomic, assign) NSUInteger secondsOfLockDelayed; 49 | /** 手势密码输入剩余次数 */ 50 | @property (nonatomic, assign) short numberOfRemainingDrawing;//remainingTimes; 51 | 52 | @property (nonatomic, assign) BOOL notFirstAutoBackupAlbum; 53 | /** 是否自动备份相册 */ 54 | @property (nonatomic, assign) BOOL autoBackupAlbum; 55 | /** 电量低于20%时是否暂停自动备份,默认YES */ 56 | @property (nonatomic, assign) BOOL stopBackupAlbumWhenLowBattery; 57 | /** 是否备份照片 */ 58 | @property (nonatomic, assign) BOOL backupPhotos; 59 | /** 是否备份视频 */ 60 | @property (nonatomic, assign) BOOL backupVideos; 61 | /** 备份文件总数 */ 62 | @property (nonatomic, assign) NSUInteger totalBackup; 63 | /** 已经备份了多少个文件 */ 64 | @property (nonatomic, assign) NSUInteger completedBackup; 65 | 66 | /** 暂停下载 */ 67 | @property (nonatomic, assign) BOOL isPauseAllDownload;//貌似没什么用了 68 | /** 暂停上传 */ 69 | @property (nonatomic, assign) BOOL isPauseAllUpload;//貌似没什么用了 70 | 71 | /** 排序方式, "name"或者"time" */ 72 | //@property (nonatomic, strong) NSString *sort; 73 | 74 | #pragma mark - 方法 75 | + (BOOL)createTable; 76 | 77 | /** 78 | 获取指定设备的所有用户 79 | 按登录时间降序排列 80 | @param mac 指定设备的Mac地址 81 | @return 所有用户 82 | */ 83 | + (NSArray *)usersForMac:(NSString *)mac; 84 | 85 | /** 86 | 获取指定设备的一个已登录过的用户 87 | 88 | @param mac 指定设备的Mac地址 89 | @param account 账号 90 | @return 用户 91 | */ 92 | + (instancetype)userForMac:(NSString *)mac account:(NSString *)account; 93 | 94 | /** 95 | 获取指定设备的最新一个登录的用户 96 | 97 | @param mac 指定设备的Mac地址 98 | @return 用户 99 | */ 100 | + (instancetype)lastLoginUserForMac:(NSString *)mac; 101 | 102 | /** 103 | 把用户数据更新到本地存储 104 | */ 105 | - (BOOL)updateToLocal; 106 | 107 | /** 108 | 更新备份数量 109 | 110 | @return 是否更新成功 111 | */ 112 | - (BOOL)updateBackupCount; 113 | 114 | @end 115 | -------------------------------------------------------------------------------- /Template/ServiceRegister.m: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceRegister.m 3 | // 组件注册 4 | // 5 | 6 | #import 7 | 8 | #import 9 | #import 10 | #import 11 | #import 12 | 13 | // 策略来自 美团技术博客:美团外卖iOS App冷启动治理 14 | // 可以规定此阶段 在 willFinishLaunchingWithOptions 中调用 15 | #define STAGE_KEY_A @"STAGE_KEY_A" 16 | // 可以规定此阶段 在 didFinishLaunchingWithOptions 中调用 17 | #define STAGE_KEY_B @"STAGE_KEY_B" 18 | // segname 19 | #define SERVICE_SEGNAME "__DATA" 20 | 21 | struct ServiceRegisterHeader { 22 | char *key; 23 | void (*function)(void); 24 | }; 25 | 26 | #define XM_SERVICE_REGISTER(key) \ 27 | static void _xm##key(void); \ 28 | __attribute__((used, section(SERVICE_SEGNAME ",__" #key ".f"))) \ 29 | static const struct ServiceRegisterHeader __H##key = (struct ServiceRegisterHeader){(char *)(&#key), (void *)(&_xm##key)}; \ 30 | static void _xm##key(void) \ 31 | 32 | // 使用方法 33 | XM_SERVICE_REGISTER(STAGE_KEY_A) { // (key)不能太长,否则 __attribute__ 会报错 34 | // 这里注册 35 | } 36 | 37 | @interface ServiceRegister : NSObject 38 | @end 39 | 40 | @implementation ServiceRegister 41 | 42 | // 需要引入: 43 | //#import 44 | //#import 45 | //#import 46 | void XMExecuteFunc(char *key) { 47 | //============================================ 48 | // Dl_info info; 49 | // // 非主程序代码使用 dladdr 获取到的 machHeader,getsectiondata 函数无法获取到数据 50 | // dladdr((const void *)&AXWLExecuteFunc, &info); 51 | // #ifdef __LP64__ 52 | // const struct mach_header_64 *machHeader = (struct mach_header_64 *)info.dli_fbase; 53 | // #else 54 | // const struct mach_header *machHeader = (struct mach_header *)info.dli_fbase; 55 | // #endif 56 | // if (NULL == machHeader) { 57 | // return; 58 | // } 59 | //============================================ 60 | 61 | // 根据自己的业务选择时用此方式,还是使用 initProphet 的方式 62 | unsigned long byteCount = 0; 63 | // _mh_execute_header 不能在库、框架或包中使用,只能在主程序代码中使用,包含在头中 64 | uint8_t * data = (uint8_t *) getsectiondata(&_mh_execute_header, SERVICE_SEGNAME, key, &byteCount); 65 | NSUInteger counter = byteCount / sizeof(struct ServiceRegisterHeader); 66 | struct ServiceRegisterHeader *items = (struct ServiceRegisterHeader *)data; 67 | for (NSUInteger idx = 0; idx < counter; ++idx) { 68 | items[idx].function(); 69 | } 70 | } 71 | 72 | + (void)executeFuncsForKey:(NSString *)key 73 | { 74 | NSString *fKey = [NSString stringWithFormat:@"__%@.f", key ?: @""]; 75 | XMExecuteFunc((char *)[fKey UTF8String]); 76 | } 77 | 78 | static const struct mach_header *smh; 79 | // 会被调用很多次 80 | static void dyld_callback(const struct mach_header* mh, intptr_t vmaddr_slide) 81 | { 82 | unsigned long byteCount = 0; 83 | #ifndef __LP64__ 84 | uintptr_t *data = (uintptr_t *) getsectiondata(mh, SEGNAME, key, &byteCount); 85 | #else 86 | uintptr_t *data = (uintptr_t *) getsectiondata((const struct mach_header_64 *)mh, SERVICE_SEGNAME, "__STAGE_KEY_A.f", &byteCount); 87 | #endif 88 | if (0 != byteCount) { 89 | NSLog(@"--%lu", byteCount); 90 | smh = mh; // 这里就能拿到正确的 91 | } 92 | } 93 | 94 | // 此方法在库、框架或包中使用,也能拿到正确的 mach_header。来自阿里的iOS组件化工具BeeHive 95 | __attribute__((constructor)) 96 | void initProphet(void) { 97 | // 注册库加载时的回调,会有很多库加载,所以会回调很多次 98 | _dyld_register_func_for_add_image(dyld_callback); 99 | } 100 | 101 | @end 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode swift 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | 93 | # General macOS 94 | .DS_Store 95 | .AppleDouble 96 | .LSOverride 97 | 98 | # Icon must end with two \r 99 | Icon 100 | 101 | 102 | # Thumbnails 103 | ._* 104 | 105 | # Files that might appear in the root of a volume 106 | .DocumentRevisions-V100 107 | .fseventsd 108 | .Spotlight-V100 109 | .TemporaryItems 110 | .Trashes 111 | .VolumeIcon.icns 112 | .com.apple.timemachine.donotpresent 113 | 114 | # Directories potentially created on remote AFP share 115 | .AppleDB 116 | .AppleDesktop 117 | Network Trash Folder 118 | Temporary Items 119 | .apdisk 120 | 121 | # vscode 122 | .vscode/* 123 | !.vscode/settings.json 124 | !.vscode/tasks.json 125 | !.vscode/launch.json 126 | !.vscode/extensions.json 127 | !.vscode/*.code-snippets 128 | 129 | # Local History for Visual Studio Code 130 | .history/ 131 | 132 | # Built Visual Studio Code Extensions 133 | *.vsix -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/XMPhotosRequestManager+Utils.m: -------------------------------------------------------------------------------- 1 | // 2 | // XMPhotosRequestManager+Utils.m 3 | 4 | #import "XMPhotosRequestManager+Utils.h" 5 | 6 | @implementation XMPhotosRequestManager (Utils) 7 | // 获取优化后的视频转向信息 8 | + (AVMutableVideoComposition *)fixedCompositionWithAsset:(AVAsset *)videoAsset 9 | { 10 | AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition]; 11 | // 视频转向 12 | int degrees = [self degressFromVideoFileWithAsset:videoAsset]; 13 | if (0 == degrees) return videoComposition; 14 | 15 | CGAffineTransform translateToCenter; 16 | CGAffineTransform mixedTransform; 17 | videoComposition.frameDuration = CMTimeMake(1, 30); 18 | NSArray *tracks = [videoAsset tracksWithMediaType:AVMediaTypeVideo]; 19 | AVAssetTrack *videoTrack = [tracks objectAtIndex:0]; 20 | if (degrees == 90) { 21 | // 顺时针旋转90° 22 | translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.height, 0.0); 23 | mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2); 24 | videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width); 25 | } else if(degrees == 180) { 26 | // 顺时针旋转180° 27 | translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.width, videoTrack.naturalSize.height); 28 | mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI); 29 | videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.width,videoTrack.naturalSize.height); 30 | } else { 31 | // 顺时针旋转270° 32 | translateToCenter = CGAffineTransformMakeTranslation(0.0, videoTrack.naturalSize.width); 33 | mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2*3.0); 34 | videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width); 35 | } 36 | AVMutableVideoCompositionInstruction *roateInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; 37 | roateInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, [videoAsset duration]); 38 | AVMutableVideoCompositionLayerInstruction *roateLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack]; 39 | [roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero]; 40 | roateInstruction.layerInstructions = @[roateLayerInstruction]; 41 | // 加入视频方向信息 42 | videoComposition.instructions = @[roateInstruction]; 43 | 44 | return videoComposition; 45 | } 46 | 47 | // 获取视频角度, 这一段好像是苹果公司Demo源码 48 | + (int)degressFromVideoFileWithAsset:(AVAsset *)asset 49 | { 50 | NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; 51 | if(tracks.count <= 0) return 0; 52 | 53 | int degress = 0; 54 | AVAssetTrack *videoTrack = [tracks objectAtIndex:0]; 55 | CGAffineTransform t = videoTrack.preferredTransform; 56 | if(t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0) { 57 | // Portrait 58 | degress = 90; 59 | } else if (t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0) { 60 | // PortraitUpsideDown 61 | degress = 270; 62 | } else if (t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0) { 63 | // LandscapeRight 64 | degress = 0; 65 | } else if (t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0) { 66 | // LandscapeLeft 67 | degress = 180; 68 | } 69 | 70 | return degress; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS_App_Template 2 | [国内链接gitee](https://gitee.com/maoxm/iOS_App_Template) 3 | iOS工具类和模板代码,简单高效 4 | 5 | ## [工具类在“/Utils/”目录下,一般可以直接使用](/Utils) 6 | 1. [线程锁:XMLock.h](/Utils/XMLock.h) 7 | 2. [常量,常用判断,MIME类型获取,调试日志:Const.h](/Utils/Const.h) 8 | 3. [国际化,本地化:LocalizedManager/](/Utils/LocalizedManager) 9 | 4. [NSInputStream添加跳过(skip)方法:NSInputStream+Skip/](/Utils/NSInputStreamSkip) 10 | 5. [工具类,计算文件MD5、创建图片缩略图、获取文件夹大小、获取可用存储空间大小、AES加解密:Utils.m](/Utils/Utils.m) 11 | 6. [随app启动自动运行一些代码,不需要开发者主动调用,适用于一些第三库自动运行](/Utils/_XMAutoLaunch.m) 12 | 7. [限制 UITextField 输入](/Utils/InputLimiter) 13 | 8. [获取视频文件的第一帧,远程或本地视频都可](/Utils/Utils.m#L390) 14 | 9. [禁止音乐远程控制](/Utils/Utils.m#L428) 15 | 10. [用最简单的方式自定义一个Toast](/Utils/Toast.m) 16 | 17 | 18 | ## [模板类在“/Template/”目录下,一般无法直接使用,主要用来参考里面的逻辑,或者直接修改模板代码](/Template) 19 | 1. [后台备份,相册备份:Backup/](/Template/Backup) 20 | 2. [后台下载:Download/](/Template/Download) 21 | 3. [后台上传:Upload/](/Template/Upload) 22 | [上传下载备份中用到的:DownloadUploadBackupCommon/](/Template/DownloadUploadBackupCommon) 23 | 4. [屏幕旋转控制:ShouldNotAutorotate/](/Template/ShouldNotAutorotate) 24 | 5. [IP地址获取,当前Wi-Fi获取,连接Wi-Fi,监听WiFi切换:IPAddr.m](/Template/IPAddr.m) 25 | 6. [UITableViewCell侧滑删除,长按事件:TableViewTemplate.m](/Template/TableViewTemplate.m) 26 | 7. [UITableViewCell高度自适应:TableViewCellAutoCalculate.m](/Template/TableViewCellAutoCalculate.m) 27 | 8. [WKWebView简单使用:WebViewController.m](/Template/WebViewController.m) 28 | 9. [身份验证,生物识别:LocalAuthentication.m](/Template/LocalAuthentication.m) 29 | 10. [分享(社会化):Share.m](/Template/Share.m) 30 | 11. [蓝牙BLE:BLE/](/Template/BLE) 31 | 12. [Swift坑爹的 ViewController的init指定构造器:Test2ViewController.swift](/Template/Test2ViewController.swift) 32 | 13. [系统日历事件和提醒:EventKitTemplate.m](/Template/EventKitTemplate.m) 33 | 14. [制作一个和启动页一模一样的页面,动态替换启动页:LaunchViewController.m](/Template/LaunchViewController.m) 34 | 15. [纯代码实现iOS原生扫描,图片二维码识别:ScanViewController.m](/Template/ScanViewController.m) 35 | 16. [文字转语音:SpeechUtils.m](/Template/SpeechUtils.m) 36 | 17. [shell自动打包脚本:iOS_App_Template-archive.sh](/iOS_App_Template-archive.sh) 37 | 18. [mDNS服务](/Template/MDNS) 38 | 19. [一次性 GCD timer](/Template/TemplateUtils.m#L36) 39 | 20. [复制到剪切板](/Template/TemplateUtils.m#L52) 40 | 21. [使用iOS原生类请求 HTTP JSON,不依赖第三方库](/Template/TemplateUtils.m#L61) 41 | 22. [从AppStore获取版App最新本号](/Template/TemplateUtils.m#L75) 42 | 23. [使用UIDocumentPickerViewController获取手机本地(File app)文件](/Template/SelectFileViewController.m) 43 | 24. [使用 NSURLSession.sharedSession 下载文件并获取进度,免得自己创建 NSURLSession 来设置 NSURLSessionDownloadDelegate 去获取进度](/Template/TemplateUtils.m#L111) 44 | 25. [UITextField 添加 leftView 文字 并且为 leftView 留空白](/Template/TemplateUtils.m#L147) 45 | 26. [设置 UIButton 图片和文字之间的间隔](/Template/TemplateUtils.m#L168) 46 | 27. [用最简单的方式仿 UIAlertController 弹框](/Template/AlertViewController.m#L23) 47 | 28. [移动 UITextField 的光标位置](/Template/TemplateUtils.m#L211) 48 | 29. [组件化服务自动注册,与分阶段启动](/Template/ServiceRegister.m) 49 | 30. [给 storyboard 或者 xib 添加国际化key填空,而不是将 storyboard 或者 xib 直接国际化,方便国际化 Localizable.strings 文件的统一管理](/Template/SwiftTemplate.swift#L11) 50 | 31. [设置 navigationBar 颜色](/Template/SwiftTemplate.swift#L40) 51 | 32. [代码生成圆角矩形图片,可拉伸](/Template/SwiftTemplate.swift#L66) 52 | 33. [圆环倒计时动画](/Template/SwiftTemplate.swift#L83) 53 | 54 | 55 | ## 长见识(自己去搜,去了解,去使用) 56 | 1. 音频控制、锁屏显示: MPRemoteCommandCenter、MPNowPlayingInfoCenter 57 | 2. 音视频播放:AVFoundation; 音视频下载缓存:AVAssetDownloadURLSession 58 | 3. HTTP上传文件的断点续传协议可参考(苹果公司为其NSURLSession上传文件定制的):https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/ 59 | 4. 苹果系统有自己的[Swift响应式框架(闭源的)](https://developer.apple.com/documentation/combine/),貌似性能比开源RX的好很多 60 | 5. 避免两个 UIButton(UIView)同时响应点击事件:通过设置 UIView 的 exclusiveTouch 为 YES,可以解决这个问题 61 | 6. Swift 异步转同步方法:withCheckedContinuation、withCheckedThrowingContinuation、withUnsafeContinuation、withUnsafeThrowingContinuation 62 | -------------------------------------------------------------------------------- /Utils/Toast.m: -------------------------------------------------------------------------------- 1 | // 2 | // Toast.m 3 | // RadarModule 4 | // 5 | // Created by macmini on 2023/9/26. 6 | // 7 | 8 | #import "Toast.h" 9 | 10 | @implementation Toast 11 | { 12 | UIButton *_lab; 13 | NSArray *_cons; 14 | NSMutableArray *_titles; 15 | BOOL _isShow; 16 | dispatch_semaphore_t _semaphore; 17 | } 18 | 19 | - (void)addToastViewTo:(UIView *)view 20 | { 21 | [self addToastViewTo:view toastLocation:0]; 22 | } 23 | 24 | - (void)addToastViewOnCenterTo:(UIView *)view 25 | { 26 | [self addToastViewTo:view toastLocation:1]; 27 | } 28 | 29 | - (void)addToastViewTo:(UIView *)view toastLocation:(int)location 30 | { 31 | if (nil != _lab) return; 32 | 33 | _semaphore = dispatch_semaphore_create(1); 34 | 35 | _lab = [UIButton buttonWithType:UIButtonTypeCustom]; 36 | _lab.userInteractionEnabled = NO; 37 | _lab.contentEdgeInsets = UIEdgeInsetsMake(10, 10, 10, 10); 38 | _lab.backgroundColor = [UIColor colorWithWhite:0 alpha:0.9]; 39 | _lab.titleLabel.font = [UIFont systemFontOfSize:16]; 40 | _lab.titleLabel.numberOfLines = 0; 41 | _lab.layer.cornerRadius = 4; 42 | _lab.alpha = 0.9; 43 | _lab.hidden = YES; 44 | [view addSubview:_lab]; 45 | 46 | [_lab setTranslatesAutoresizingMaskIntoConstraints:NO]; 47 | NSLayoutConstraint *lc1 = [NSLayoutConstraint constraintWithItem:_lab attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]; 48 | NSLayoutConstraint *lc2; 49 | if (0 == location) { 50 | lc2 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottomMargin relatedBy:NSLayoutRelationEqual toItem:_lab attribute:NSLayoutAttributeBottom multiplier:1 constant:50]; 51 | } else { 52 | lc2 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_lab attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]; 53 | } 54 | NSLayoutConstraint *lc3 = [NSLayoutConstraint constraintWithItem:_lab attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:view attribute:NSLayoutAttributeLeadingMargin multiplier:1 constant:20]; 55 | NSLayoutConstraint *lc4 = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTrailingMargin relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:_lab attribute:NSLayoutAttributeTrailing multiplier:1 constant:20]; 56 | _cons = @[lc1, lc2, lc3, lc4]; 57 | [view addConstraints:_cons]; 58 | } 59 | 60 | - (void)removeToastViewFromSuperview 61 | { 62 | if (nil != _titles) [_titles removeAllObjects]; 63 | if (nil == _lab) return; 64 | 65 | [_lab.superview removeConstraints:_cons]; 66 | [_lab removeFromSuperview]; 67 | _lab = nil; 68 | _cons = nil; 69 | } 70 | 71 | - (void)show:(NSString *)text 72 | { 73 | dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); 74 | if (nil == _titles) { 75 | _titles = [NSMutableArray arrayWithCapacity:5]; 76 | } 77 | [_titles addObject:text]; 78 | dispatch_semaphore_signal(_semaphore); 79 | if (_isShow) return; 80 | 81 | [self showToast]; 82 | } 83 | 84 | - (void)showToast 85 | { 86 | if (_titles.count == 0 || nil == _lab) return;// || nil == _lab.superview.window 87 | 88 | dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); 89 | NSString *text = _titles.firstObject; 90 | [_titles removeObjectAtIndex:0]; 91 | dispatch_semaphore_signal(_semaphore); 92 | 93 | _isShow = YES; 94 | _lab.hidden = !_isShow; 95 | _lab.alpha = 0; 96 | [_lab setTitle:text forState:UIControlStateNormal]; 97 | [_lab.superview bringSubviewToFront:_lab]; 98 | UIButton *lab = _lab; 99 | [UIView animateWithDuration:.3 animations:^{ 100 | self->_lab.alpha = .9; 101 | } completion:^(BOOL finished) { 102 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 103 | [self dismissToast:lab]; 104 | }); 105 | }]; 106 | } 107 | 108 | - (void)dismissToast:(UIButton *)lab 109 | { 110 | [UIView animateWithDuration:.3 animations:^{ 111 | lab.alpha = 0; 112 | } completion:^(BOOL finished) { 113 | self->_isShow = NO; 114 | lab.hidden = !self->_isShow; 115 | [self showToast]; 116 | }]; 117 | } 118 | 119 | @end 120 | -------------------------------------------------------------------------------- /Template/Webview/URLSchemeHandler.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | 21 | #import "URLSchemeHandler.h" 22 | #import 23 | 24 | #import 25 | 26 | @implementation URLSchemeHandler 27 | 28 | 29 | - (instancetype)initWithVC:(UIViewController *)controller 30 | { 31 | self = [super init]; 32 | if (self) { 33 | _viewController = controller; 34 | } 35 | return self; 36 | } 37 | 38 | - (void)webView:(WKWebView *)webView startURLSchemeTask:(id )urlSchemeTask 39 | { 40 | NSString * startPath = [[NSBundle mainBundle] pathForResource:@"www" ofType: nil]; 41 | NSURL * url = urlSchemeTask.request.URL; 42 | NSString * stringToLoad = url.path; 43 | NSString * scheme = url.scheme; 44 | // 45 | if ([scheme isEqualToString:@"app"]) { 46 | if ([stringToLoad hasPrefix:@"/_app_file_"]) { 47 | startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""]; 48 | } else { 49 | if ([stringToLoad isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) { 50 | startPath = [startPath stringByAppendingPathComponent:@"你的首页地址"]; 51 | } else { 52 | startPath = [startPath stringByAppendingPathComponent:stringToLoad]; 53 | } 54 | } 55 | } 56 | 57 | // 获取本地文件数据 58 | NSError * fileError = nil; 59 | NSData * data = nil; 60 | if ([self isMediaExtension:url.pathExtension]) { 61 | data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError]; 62 | } 63 | if (!data || fileError) { 64 | data = [[NSData alloc] initWithContentsOfFile:startPath]; 65 | } 66 | NSInteger statusCode = 200; 67 | if (!data) { 68 | statusCode = 404; 69 | } 70 | NSURL * localUrl = [NSURL URLWithString:url.absoluteString]; 71 | NSString * mimeType = [self getMimeType:url.pathExtension]; 72 | id response = nil; 73 | if (data && [self isMediaExtension:url.pathExtension]) { 74 | response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil]; 75 | } else { 76 | NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"}; 77 | response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers]; 78 | } 79 | 80 | // 这几行是关键 81 | [urlSchemeTask didReceiveResponse:response]; 82 | if (data) { 83 | [urlSchemeTask didReceiveData:data]; 84 | } 85 | [urlSchemeTask didFinish]; 86 | } 87 | 88 | - (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id)urlSchemeTask 89 | { 90 | // SEL selector = NSSelectorFromString(@"stopSchemeTask:"); 91 | // if (self.schemePlugin != nil && [self.schemePlugin respondsToSelector:selector]) { 92 | // (((void (*)(id, SEL, id ))objc_msgSend)(self.schemePlugin, selector, urlSchemeTask)); 93 | // } 94 | } 95 | 96 | - (NSString *)getMimeType:(NSString *)fileExtension 97 | { 98 | if (fileExtension && ![fileExtension isEqualToString:@""]) { 99 | NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL); 100 | NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType); 101 | return contentType ? contentType : @"application/octet-stream"; 102 | } else { 103 | return @"text/html"; 104 | } 105 | } 106 | 107 | - (BOOL)isMediaExtension:(NSString *)pathExtension 108 | { 109 | NSArray * mediaExtensions = @[@"m4v", @"mov", @"mp4", @"aac", @"ac3", @"aiff", @"au", @"flac", @"m4a", @"mp3", @"wav"]; 110 | return [mediaExtensions containsObject:pathExtension.lowercaseString]; 111 | } 112 | 113 | @end 114 | -------------------------------------------------------------------------------- /Template/SwiftTemplate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftTemplate.swift 3 | // iOS_App_Template 4 | // 5 | // Created by macmini on 2025/1/9. 6 | // Copyright © 2025 mxm. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIButton { 12 | /* 13 | 给 storyboard 或者 xib 添加可设置国际化key,而不是将 storyboard 或者 xib 直接国际化, 14 | 方便国际化 Localizable.strings 文件的管理。 15 | 可以在 storyboard 或者 xib 的 属性面板上看到此填空 16 | */ 17 | @IBInspectable public var normalTitleLocalizedKey: String { 18 | get { 19 | return "" 20 | } 21 | set { 22 | setTitle(NSLocalizedString(newValue, comment: ""), for: .normal) 23 | } 24 | } 25 | } 26 | 27 | extension UILabel { 28 | // 给 storyboard 或者 xib 添加可设置国际化key 29 | @IBInspectable public var textLocalizedKey: String { 30 | get { 31 | return "" 32 | } 33 | set { 34 | text = NSLocalizedString(newValue, comment: "") 35 | } 36 | } 37 | } 38 | 39 | extension UIViewController { 40 | func adjustNavigationBarAppearance() { 41 | let barBgColor = UIColor(named: "view_bg")! 42 | let titleColor = UIColor(named: "title1_color")! 43 | let titleTextAttributes = [NSAttributedString.Key.foregroundColor : titleColor] 44 | if #available(iOS 13.0, *) { 45 | let appearance = UINavigationBarAppearance() 46 | appearance.configureWithOpaqueBackground() 47 | appearance.backgroundColor = barBgColor 48 | // 设置title颜色 49 | appearance.titleTextAttributes = titleTextAttributes; 50 | // let itemAppearance = UIBarButtonItemAppearance(style: .plain) 51 | // itemAppearance.normal.titleTextAttributes = titleTextAttributes 52 | // appearance.backButtonAppearance = itemAppearance 53 | navigationController?.navigationBar.standardAppearance = appearance 54 | navigationController?.navigationBar.scrollEdgeAppearance = appearance 55 | } else { 56 | navigationController?.navigationBar.titleTextAttributes = titleTextAttributes 57 | navigationController?.navigationBar.barTintColor = barBgColor 58 | } 59 | // 设置item的颜色,包括字体颜色和图片颜色: topItem, backItem, rightBarButtonItem 60 | navigationController?.navigationBar.tintColor = titleColor 61 | } 62 | } 63 | 64 | extension UIImage { 65 | // 生成圆角矩形图片,可拉伸 66 | static func resizableRoundedRectImage(size: CGSize, color: UIColor, cornerRadius: CGFloat) -> UIImage? { 67 | let image = UIGraphicsImageRenderer(size: size).image { context in 68 | // 设置抗锯齿 69 | context.cgContext.setAllowsAntialiasing(true) 70 | context.cgContext.setShouldAntialias(true) 71 | // 高质量插值计算 72 | context.cgContext.interpolationQuality = .high 73 | let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height), cornerRadius: cornerRadius) 74 | path.addClip() 75 | color.setFill() 76 | path.fill() // 区域内填充 77 | } 78 | return image.resizableImage(withCapInsets: UIEdgeInsets(top: 0, left: cornerRadius, bottom: 0, right: cornerRadius), resizingMode: .stretch) 79 | } 80 | } 81 | 82 | // 倒计时环动画 83 | class DownCountUtils { 84 | private static let key = "downCount" 85 | 86 | // 生成一个圆环 87 | static func downCountCircular(frame: CGRect, radius: CGFloat, color: UIColor) -> CAShapeLayer { 88 | let bezierPath = UIBezierPath(arcCenter: CGPoint(x: radius, y: radius), 89 | radius: radius, 90 | startAngle: .pi * 1.5, 91 | endAngle: .pi * -0.5, 92 | clockwise: false) 93 | 94 | let layer = CAShapeLayer() 95 | layer.frame = frame 96 | layer.fillColor = UIColor.clear.cgColor 97 | layer.strokeColor = color.cgColor 98 | layer.lineWidth = 10 99 | layer.lineCap = .round 100 | layer.path = bezierPath.cgPath 101 | layer.strokeEnd = 0.0 102 | return layer 103 | } 104 | 105 | // 给上面生成的圆环,添加倒计时消失动画 106 | /// - parameters: 107 | /// - totalTime: 总时间,单位:秒. 108 | /// - remainingTime: 剩余时间,单位:秒. 109 | static func startDownCountAnimation(_ layer: CALayer, totalTime: UInt, remainingTime: UInt) { 110 | let animation = CABasicAnimation(keyPath: "strokeEnd") 111 | animation.fromValue = CGFloat(remainingTime) / CGFloat(totalTime) 112 | animation.toValue = CGFloat(0) 113 | animation.duration = CFTimeInterval(remainingTime) 114 | animation.isRemovedOnCompletion = false 115 | layer.add(animation, forKey: key) 116 | } 117 | // 删除动画 118 | static func stopDownCountAnimation(_ layer: CALayer) { 119 | layer.removeAnimation(forKey: key) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Template/AlertViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // AlertViewController.m 3 | // 4 | 5 | #import 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | @interface AlertViewController : UIViewController 10 | 11 | @end 12 | 13 | NS_ASSUME_NONNULL_END 14 | 15 | @interface AlertViewController () 16 | 17 | @end 18 | 19 | @implementation AlertViewController 20 | { 21 | // 自定义的弹框,弹框上面可能有 UITextField 22 | UIView *_alertView; 23 | } 24 | 25 | // 下层的 ViewController 可以直接调用 [self presentViewController: animated:YES completion:] 来呈现此 ViewController,就和呈现 UIAlertController 一样 26 | - (instancetype)init 27 | { 28 | self = [super init]; 29 | if (self) { 30 | // 这两点设置非常重要,storyboard 上也可以设置 31 | // 这个最好设置成带 Over 的选项,加上半透明背景可保证能看到下层的 ViewController。PageSheet 和 FormSheet 也许也有这种效果,请自行百度他们的效果。 32 | self.modalPresentationStyle = UIModalPresentationOverFullScreen; 33 | // 这个是使用渐变转场动画,请自行百度他们的效果 34 | self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; 35 | } 36 | return self; 37 | } 38 | 39 | - (void)viewDidLoad 40 | { 41 | [super viewDidLoad]; 42 | 43 | // 请自定义你的弹框 44 | _alertView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 270, 200)]; 45 | [self.view addSubview:_alertView]; 46 | // 背景使用有透明度的黑色,保证可以看到下层的 ViewController 47 | self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2]; 48 | // 增加监听,当键盘出现或改变时收出消息 49 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; 50 | // 增加监听,当键退出时收出消息 51 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; 52 | } 53 | 54 | - (void)viewWillAppear:(BOOL)animated 55 | { 56 | [super viewWillAppear:animated]; 57 | // 这里给弹框添加动画,可以仿 UIAlertController 动画 58 | /* 59 | // 比例缩放 60 | CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; 61 | // 1秒后执行 62 | // animation.beginTime = CACurrentMediaTime() + 0.1; 63 | // 持续时间 64 | // animation.duration = 0.4; 65 | // 重复次数 66 | // animation.repeatCount = 1; 67 | // 起始scale 68 | animation.fromValue = @(1.18); 69 | // 终止scale 70 | animation.toValue = @(1); 71 | animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; 72 | // animation.fillMode = kCAFillModeRemoved; 73 | 74 | // 透明度缩放 75 | CABasicAnimation *animationOpacity = [CABasicAnimation animationWithKeyPath:@"opacity"]; 76 | // 1秒后执行 77 | // animationOpacity.beginTime = CACurrentMediaTime() + 0.1; 78 | // 持续时间 79 | // animationOpacity.duration = 0.4; 80 | // 重复次数 81 | // animationOpacity.repeatCount = 1; 82 | // 起始scale 83 | animationOpacity.fromValue = @(0.0); 84 | // 终止scale 85 | animationOpacity.toValue = @(1); 86 | animationOpacity.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; 87 | // animationOpacity.fillMode = kCAFillModeRemoved; 88 | 89 | // 动画组 90 | CAAnimationGroup *group = [CAAnimationGroup animation]; 91 | group.animations = @[animation, animationOpacity]; 92 | // 持续时间 93 | group.duration = 0.4; 94 | // 动画结束是否恢复原状 95 | group.removedOnCompletion = YES; 96 | // 动画组 97 | group.fillMode = kCAFillModeRemoved; 98 | // 添加动画 99 | [_alertView.layer addAnimation:group forKey:@"group"];//*/ 100 | } 101 | 102 | - (void)dealloc 103 | { 104 | [NSNotificationCenter.defaultCenter removeObserver:self name:UIKeyboardWillShowNotification object:nil]; 105 | [NSNotificationCenter.defaultCenter removeObserver:self name:UIKeyboardWillHideNotification object:nil]; 106 | NSLog(@"%@ dealloc", NSStringFromClass([self class])); 107 | } 108 | 109 | // 弹框上有输入框时添加动画 110 | - (void)keyboardWillShow:(NSNotification *)aNotification 111 | { 112 | // 获取键盘的高度 113 | NSValue *aValue = aNotification.userInfo[UIKeyboardFrameEndUserInfoKey]; 114 | CGRect keyboardRect = [aValue CGRectValue]; 115 | NSLog(@"%@", NSStringFromCGRect(keyboardRect)); 116 | 117 | // CGPoint center = self.view.center; 118 | // center.y -= CGRectGetHeight(keyboardRect)/2; 119 | [UIView animateWithDuration:.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ 120 | // 这种平移会有bug:当点击弹框上的按钮时动画会自动还原;而且只能在 viewDidAppear 中调用 becomeFirstResponder,否则会有bug 121 | // self->_alertView.center = center; 122 | // 这种不限制在哪调用 becomeFirstResponder 123 | self->_alertView.transform = CGAffineTransformMakeTranslation(0, -CGRectGetHeight(keyboardRect)/2); 124 | } completion:nil]; 125 | } 126 | 127 | // 键盘消失 128 | - (void)keyboardWillHide:(NSNotification *)aNotification 129 | { 130 | // CGPoint center = self.view.center; 131 | [UIView animateWithDuration:.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ 132 | // self->_alertView.center = center; 133 | self->_alertView.transform = CGAffineTransformIdentity; 134 | } completion:nil]; 135 | } 136 | 137 | @end 138 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/FileTask.h: -------------------------------------------------------------------------------- 1 | // 2 | // FileTask.h 3 | // 使用的是WCDB框架 4 | 5 | #import 6 | #import 7 | 8 | //此类的成员的顺序千万不要改变,因为判断时用到了'<''>'做比较 9 | typedef NS_ENUM(NSInteger, FileTaskStatus) { 10 | FileTaskStatusWaiting = 0, //等待 11 | FileTaskStatusExporting, //导出中 12 | FileTaskStatusExported, //已导出 13 | FileTaskStatusInProgress, //传输进行中 14 | FileTaskStatusPause, //暂停 15 | FileTaskStatusCompleted, //完成 16 | FileTaskStatusError, //出错 17 | // FileTaskStatusCanceled, //取消 18 | FileTaskStatusDeleted 19 | }; 20 | 21 | typedef NS_ENUM(NSInteger, FileTaskType) { 22 | FileTaskTypeUpload, //上传 23 | FileTaskTypeDownload,//下载 24 | }; 25 | 26 | @class User; 27 | 28 | @interface FileTask : NSObject 29 | /** 主键,自增 */ 30 | @property (nonatomic, assign, readonly) NSUInteger Id; 31 | /** 主机mac地址 */ 32 | @property (nonatomic, copy) NSString *mac; 33 | /** 用户id */ 34 | @property (nonatomic, assign) int userId; 35 | /** 文件名称 */ 36 | @property (nonatomic, copy) NSString *fileName; 37 | /** 文件后缀, extension name */ 38 | @property (nonatomic, copy) NSString *fileExt; 39 | /** 文件类型,图片或视频 */ 40 | @property (nonatomic, assign) PHAssetMediaType mediaType; 41 | /** 传输状态 */ 42 | @property (nonatomic, assign) FileTaskStatus state;//成功,失败,完成,暂停 43 | /** 文件大小,单位byte */ 44 | @property (nonatomic, assign) uint64_t size; 45 | /** 已传输文件大小 */ 46 | @property (nonatomic, assign) uint64_t completedSize; 47 | /** 传输类型 */ 48 | @property (nonatomic, assign) FileTaskType type;//上传下载 49 | /** 在server上的路径,也是上传目标路径 */ 50 | @property (nonatomic, copy) NSString *serverPath; 51 | /** 文件创建时间 */ 52 | @property (nonatomic, assign) NSTimeInterval createTime; 53 | /** 本地沙盒缓存相对路径,若是FileTaskTypeDownload则相对于NSDocumentDirectory */ 54 | @property (nonatomic, copy) NSString *localPath; 55 | /** 当前传输的片段,从0开始 */ 56 | @property (nonatomic, assign) uint32_t currentFragment; 57 | /** 总片段 */ 58 | @property (nonatomic, assign) uint32_t totalFragment; 59 | /** 文件处理 */ 60 | @property (nonatomic, strong) NSFileHandle *fileHandle; 61 | /** 可以重试一次上传 */ 62 | @property (nonatomic, assign) BOOL canUpload; 63 | /** PHAsset.localIdentifier */ 64 | @property (nonatomic, copy) NSString *assetLocalIdentifier; 65 | /** 文件类型 */ 66 | @property (nonatomic, assign) FileType filetype; 67 | 68 | //------------------这几个不用保存到数据库------------------------- 69 | /** 相册文件的asset */ 70 | @property (nonatomic, strong) PHAsset *asset; 71 | /** 选中状态 */ 72 | @property (nonatomic, assign, getter=isSelected) BOOL selected; 73 | /** 传输速度 */ 74 | @property (nonatomic, assign) uint64_t transmissionSpeed; 75 | 76 | /** 创建时间,格式化好的字符串 */ 77 | @property (nonatomic, readonly) NSString *createTimeFormatString; 78 | /** 传输速度,格式化好的字符串 */ 79 | @property (nonatomic, readonly) NSString *speedFormatString; 80 | /** 文件大小,格式化好的字符串 */ 81 | @property (nonatomic, copy) NSString *sizeFormatString; 82 | /** 已传输的大小,格式化好的字符串 */ 83 | @property (nonatomic, readonly) NSString *completedSizeFormatString; 84 | /** 本地沙盒缓存绝对路径 */ 85 | @property (nonatomic, copy) NSString *absoluteLocalPath; 86 | /** 断点续传的数据 */ 87 | @property (nonatomic, copy) NSString *resumeDataName; 88 | 89 | + (BOOL)createTable; 90 | 91 | /** 92 | 获取数据库中最大的Id 93 | 94 | @return Id 95 | */ 96 | + (NSInteger)fileTaskMaxId; 97 | 98 | /** 99 | 正在下载,或者正在上传的任务,按id升序排列 100 | 101 | @param user 用户 102 | @param type 任务类型 103 | @return 任务 104 | */ 105 | + (NSArray *)progressFileTasksForUser:(User *)user taskType:(FileTaskType)type; 106 | 107 | + (NSArray *)progressFileTasksForUser:(User *)user taskType:(FileTaskType)type offset:(NSUInteger)offset; 108 | 109 | /** 110 | 获取大于自定id的任务,且是正在下载,或者正在上传的任务 111 | 112 | @param user 用户 113 | @param type 任务类型 114 | @param Id fileTask.Id 115 | @return 任务 116 | */ 117 | + (NSArray *)progressFileTasksForUser:(User *)user taskType:(FileTaskType)type idGreaterThan:(NSInteger)Id; 118 | 119 | /** 120 | 已成功的任务 121 | 122 | @param user 用户 123 | @param type 任务类型 124 | @return 任务 125 | */ 126 | + (NSArray *)successFileTasksForUser:(User *)user taskType:(FileTaskType)type; 127 | /** 128 | 已失败的任务 129 | 130 | @param user 用户 131 | @param type 任务类型 132 | @return 任务 133 | */ 134 | + (NSArray *)failureFileTasksForUser:(User *)user taskType:(FileTaskType)type; 135 | 136 | + (BOOL)isExistsFileTaskForUser:(User *)user serverPath:(NSString *)serverPath fileTaskType:(FileTaskType)type; 137 | 138 | /** 139 | 添加新任务 140 | 141 | @param fileTasks 多个任务 142 | @return 是否添加成功 143 | */ 144 | + (BOOL)addFileTasks:(NSArray *)fileTasks; 145 | 146 | /** 147 | 删除用户所有数据 148 | 149 | @return 是否删除成功 150 | */ 151 | + (BOOL)deleteAllFileTasksForUser:(User *)user forType:(FileTaskType)type; 152 | 153 | + (BOOL)deleteFileTask:(FileTask *)ftask; 154 | 155 | + (BOOL)deleteFileTaskWithIDs:(NSArray *)ids; 156 | 157 | + (BOOL)updateFileTasks:(NSArray *)fileTasks; 158 | 159 | 160 | /** 161 | 新数据插入到数据库使用的初始化方法 162 | 163 | @return FileTask 164 | */ 165 | - (instancetype)initForInsert; 166 | 167 | /** 168 | 把用户数据更新到数据库,
当数据库不存在这条数据,且使用了initForInsert初始化实例,则会插入一条数据,否则会更新数据库 169 | */ 170 | - (BOOL)updateToLocal; 171 | 172 | - (BOOL)updateStatusToLocal; 173 | 174 | @end 175 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/TransferCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // TransferCell.m 3 | // 4 | // 5 | // Created by mxm on 2018/4/24. 6 | // Copyright © 2018年 mxm. All rights reserved. 7 | // 8 | 9 | #import "TransferCell.h" 10 | #import "FileTask.h" 11 | #import "MarqueeLabel.h" 12 | #import "UIImage+Format.h" 13 | #import "UIFont+TMCustomFont.h" 14 | 15 | static CGFloat const ByteLabelW = 70; 16 | 17 | @implementation TransferCell 18 | { 19 | MarqueeLabel *_labMar;//跑马灯控件 20 | // UIButton *_btn; 21 | UIImageView *_imgView; 22 | FileTask *_task; 23 | } 24 | 25 | + (instancetype)dequeueReusableCellWithTableView:(UITableView *)tableView 26 | { 27 | static NSString *const identifier = @"TransferCellIdentifier"; 28 | TransferCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; 29 | if (!cell) { 30 | cell = [[TransferCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; 31 | cell.textLabel.font = [UIFont systemFontOfSize:16]; 32 | cell.detailTextLabel.font = [UIFont systemFontOfSize:14]; 33 | cell.byteLabel.font = [UIFont systemFontOfSize:14]; 34 | cell.byteLabel.textAlignment = NSTextAlignmentRight; 35 | } 36 | return cell; 37 | } 38 | 39 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 40 | { 41 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 42 | if (self) { 43 | _byteLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, ByteLabelW, 20)]; 44 | [self.contentView addSubview:_byteLabel]; 45 | 46 | _labMar = [MarqueeLabel new]; 47 | _labMar.font = [UIFont systemFontOfSize:16]; 48 | [self.contentView addSubview:_labMar]; 49 | 50 | _imgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"translate_all_stop.png"]]; 51 | } 52 | return self; 53 | } 54 | 55 | - (void)btnAction 56 | { 57 | 58 | } 59 | 60 | - (void)layoutSubviews 61 | { 62 | //利用super计算textLabel,_labMar使用textLabel的frame 63 | self.textLabel.hidden = NO; 64 | _labMar.text = self.textLabel.text; 65 | [super layoutSubviews]; 66 | _labMar.frame = self.textLabel.frame; 67 | self.textLabel.hidden = YES; 68 | 69 | CGRect frame; 70 | CGFloat space = 15;//_byteLabel右边的空隙 71 | if (nil != self.accessoryView) { 72 | // frame = self.accessoryView.frame; 73 | // frame.origin.x += 10; 74 | // self.accessoryView.frame = frame; 75 | 76 | space = 5; 77 | } 78 | CGFloat w = CGRectGetWidth(self.contentView.bounds); 79 | frame = self.detailTextLabel.frame; 80 | frame.origin.x = w - ByteLabelW - space; 81 | frame.size.width = ByteLabelW; 82 | _byteLabel.frame = frame; 83 | } 84 | 85 | - (void)setFileTask:(FileTask *)task 86 | { 87 | _task = task; 88 | self.textLabel.text = task.fileName; 89 | 90 | switch (task.state) { 91 | //这几个是非传输状态 92 | case FileTaskStatusCompleted: 93 | case FileTaskStatusError: 94 | case FileTaskStatusDeleted: 95 | self.detailTextLabel.text = task.createTimeFormatString;//时间 96 | self.byteLabel.text = task.sizeFormatString;//大小 97 | self.accessoryView = nil; 98 | break; 99 | 100 | default://剩下的是传输状态 101 | { 102 | self.detailTextLabel.text = [NSString stringWithFormat:@"%@/%@", task.completedSizeFormatString, task.sizeFormatString];//大小 103 | if (FileTaskTypeUpload == task.type) { 104 | self.accessoryView = nil;//上传是不能暂停的,所以不需要图片 105 | } else { 106 | self.accessoryView = _imgView; 107 | } 108 | 109 | switch (task.state) { 110 | case FileTaskStatusPause: 111 | _byteLabel.text = nil; 112 | if (FileTaskTypeDownload == task.type) { 113 | _imgView.image = [UIImage imageNamed:@"translate_all_begin.png"];//下载图片 114 | }//else //上传图片 115 | break; 116 | 117 | case FileTaskStatusInProgress: 118 | _byteLabel.text = task.speedFormatString;//速度 119 | _imgView.image = [UIImage imageNamed:@"translate_all_stop.png"];//暂停图片 120 | break; 121 | 122 | default://剩下的三种Waiting,Exporting,Exported 123 | _byteLabel.text = @"waiting";//@"等待中..."; 124 | _imgView.image = [UIImage imageNamed:@"translate_all_stop.png"];//暂停图片 125 | break; 126 | } 127 | }//default 128 | break; 129 | } 130 | } 131 | 132 | - (void)setPausedStatus:(BOOL)isPaused 133 | { 134 | if (FileTaskTypeUpload == _task.type) { 135 | self.accessoryView = nil;//上传是不能暂停的,所以不需要图片 136 | } else { 137 | self.accessoryView = _imgView; 138 | } 139 | if (isPaused) { 140 | _byteLabel.text = nil; 141 | if (FileTaskTypeDownload == _task.type) { 142 | _imgView.image = [UIImage imageNamed:@"translate_all_begin.png"];//下载图片 143 | }//else //上传图片 144 | } else { 145 | _imgView.image = [UIImage imageNamed:@"translate_all_stop.png"];//暂停图片 146 | } 147 | } 148 | 149 | - (void)setImageWithFileTask:(FileTask *)task 150 | { 151 | if (nil == task.fileExt) { 152 | task.fileExt = [task.fileName pathExtension]; 153 | } 154 | self.imageView.image = [UIImage imageNamed:task.fileExt];//根据后缀显示不同的图片 155 | } 156 | 157 | @end 158 | -------------------------------------------------------------------------------- /Utils/LocalizedManager/LocalizedManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedManager.m 3 | // 4 | // Created by mxm on 2018/5/7. 5 | // Copyright © 2018 mxm. All rights reserved. 6 | // 7 | 8 | #import "LocalizedManager.h" 9 | #import "Const.h" 10 | 11 | static NSString *const LanguageBundleKey = @"k.com.mxm.xxx.LanguageBundle"; 12 | static NSString *const CurrentLanguageKey = @"k.com.mxm.xxx.CurrentLanguage"; 13 | 14 | static NSBundle *currentBundle; 15 | static dispatch_once_t onceToken; 16 | NSBundle * LMCurrentBundle(void) { 17 | dispatch_once(&onceToken, ^{ 18 | currentBundle = NSBundle.mainBundle; 19 | NSString *lb = [NSUserDefaults.standardUserDefaults stringForKey:LanguageBundleKey]; 20 | if (nil != lb) { 21 | NSString *path = [currentBundle pathForResource:lb ofType:@"lproj"]; 22 | if (nil != path) { 23 | currentBundle = [NSBundle bundleWithPath:path]; 24 | } 25 | } 26 | }); 27 | return currentBundle; 28 | } 29 | 30 | NSNotificationName const ChangeLanguageNotification = @"n.com.mxm.xxx.ChangeLanguage"; 31 | 32 | @implementation LocalizedManager 33 | 34 | static NSMutableArray *supportedLanguages; 35 | static NSDictionary *dict; 36 | 37 | + (void)initLang 38 | { 39 | static NSString *directoryKey = @"directory"; //语言目录 40 | static NSString *languageKey = @"language"; //语言 41 | 42 | static dispatch_once_t langOnceToken; 43 | dispatch_once(&langOnceToken, ^{ 44 | NSDictionary *dt = [NSMutableDictionary dictionaryWithContentsOfFile:[NSBundle.mainBundle pathForResource:@"SupportLanguages.plist" ofType:nil]]; 45 | supportedLanguages = dt[languageKey]; 46 | [supportedLanguages insertObject:NSLocalizedString(@"auto", @"跟随系统") atIndex:0];//测试这里会不会报错? 47 | dict = dt[directoryKey]; 48 | }); 49 | } 50 | 51 | + (NSArray *)supportedLanguages 52 | { 53 | [self initLang]; 54 | return supportedLanguages; 55 | /* 56 | return @[ 57 | NSLocalizedString(@"auto", @"跟随系统"),//注意,这里是NS 58 | @"English", 59 | @"简体中文", 60 | @"繁體中文", 61 | @"Deutsch", 62 | @"Español", 63 | @"Français", 64 | @"Italiano", 65 | @"Magyar", 66 | @"日本語", 67 | @"한국어" 68 | ];数组可以放到本地plist文件中*/ 69 | } 70 | 71 | /** 72 | 切换语言 73 | 74 | @param language in LocalizedManager.supportedLanguages 75 | */ 76 | + (void)changeTo:(NSString *)language 77 | { 78 | NSUserDefaults *ud = NSUserDefaults.standardUserDefaults; 79 | if ([language isEqualToString:[ud stringForKey:CurrentLanguageKey]]) { 80 | return;//未切换成新语言 81 | } 82 | /* 83 | //value是.lproj包的名字 84 | NSDictionary *dict = @{//这里不需要auto 85 | @"English" : @"Base", 86 | @"简体中文" : @"zh-hans", 87 | @"繁體中文" : @"zh-hant", 88 | @"Deutsch" : @"de", 89 | @"Español" : @"es", 90 | @"Français" : @"fr", 91 | @"Magyar" : @"hu", //这个是匈牙利语 92 | @"Italiano" : @"it", 93 | @"日本語" : @"ja", 94 | @"한국어" : @"ko", 95 | };字典可以放到本地plist文件中*/ 96 | 97 | [self initLang]; 98 | [ud setObject:dict[language] forKey:LanguageBundleKey]; 99 | [ud setObject:language forKey:CurrentLanguageKey]; 100 | [ud synchronize]; 101 | onceToken = 0; 102 | currentBundle = nil; 103 | 104 | [[NSNotificationCenter defaultCenter] postNotificationName:ChangeLanguageNotification object:nil]; 105 | } 106 | 107 | + (NSUInteger)currentLanguageIndexInSupportedLanguages 108 | { 109 | NSString *lang = [NSUserDefaults.standardUserDefaults stringForKey:CurrentLanguageKey]; 110 | if (nil == lang) return 0; 111 | 112 | NSUInteger index = [[self supportedLanguages] indexOfObject:lang]; 113 | if (NSNotFound == index) return 0; 114 | 115 | return index; 116 | } 117 | 118 | - (void)aTest 119 | { 120 | // NSLog(@"%@", [NSUserDefaults standardUserDefaults].dictionaryRepresentation);//AppleLanguages也可切换国际化,不过要重启App 121 | // NSLog(@"%@", NSLocale.availableLocaleIdentifiers); 122 | // NSLog(@"%@", NSLocale.preferredLanguages); 123 | // NSLog(@"%@", NSBundle.mainBundle.preferredLocalizations); 124 | /* NSBundle.mainBundle.localizations是本地化".lproj"包的名称, 125 | 而且全部包含在NSLocale.availableLocaleIdentifiers中 126 | */ 127 | NSLog(@"%@", NSBundle.mainBundle.localizations); 128 | NSLocale *loca = [NSLocale localeWithLocaleIdentifier:NSBundle.mainBundle.localizations[0]]; 129 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hant_MO"]); 130 | 131 | loca = NSLocale.autoupdatingCurrentLocale; 132 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hans_SG"]); 133 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hant_TW"]); 134 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hant_MO"]); 135 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hans_MO"]); 136 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hant_HK"]); 137 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hans_HK"]); 138 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hans_CN"]); 139 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hans"]); 140 | NSLog(@"%@", [loca localizedStringForLocaleIdentifier:@"zh_Hant"]); 141 | 142 | NSBundle *bun = [NSBundle bundleWithPath:[NSBundle.mainBundle pathForResource:@"ja" ofType:@"lproj"]]; 143 | 144 | NSLog(@"%@", bun.bundleIdentifier); 145 | NSLog(@"%@", [bun localizedStringForKey:@"title" value:@"" table:nil]); 146 | NSLog(@"%@", [[NSBundle bundleWithIdentifier:@"ja"] localizedStringForKey:@"title" value:@"" table:nil]); 147 | } 148 | 149 | @end 150 | -------------------------------------------------------------------------------- /iOS_App_Template/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // iOS_App_Template 4 | // 5 | // Created by mxm on 2018/1/22. 6 | // Copyright © 2019 mxm. All rights reserved. 7 | // 8 | /** 9 | os_log是apple推出的新日志系统,iOS14之后Swift可用新的Logger类,这些日志性能比NSLog好;而os_signpost结合TimeProfile在性能优化的数据展示中能够更加直观、方便, 10 | https://www.jianshu.com/p/4c112d8506ad 11 | https://blog.csdn.net/tugele/article/details/81252603 , https://www.avanderlee.com/debugging/oslog-unified-logging/ 12 | https://blog.csdn.net/weixin_26638123/article/details/108171733 13 | https://wellphone.netlify.app/post/2019/introduction_to_the_log_library_of_ios_platform/ 14 | 一些可用的日志工具:可在github上搜索 iOS + log、logger、logging、debug 15 | https://github.com/CocoaDebug/CocoaDebug 16 | https://github.com/FLEXTool/FLEX 17 | https://github.com/alibaba/youku-sdk-tool-woodpecker 18 | https://github.com/DamonHu/HDWindowLogger#chinese 19 | https://github.com/kean/Pulse 20 | https://github.com/HDB-Li/LLDebugTool 21 | https://github.com/pmusolino/Wormholy 22 | https://github.com/ripperhe/Debugo 23 | https://github.com/bytedance/flutter_ume 24 | https://github.com/dbukowski/DBDebugToolkit 25 | https://github.com/meitu/MTHawkeye/blob/develop/Readme-cn.md 26 | https://github.com/willowtreeapps/Hyperion-iOS 27 | https://github.com/indragiek/InAppViewDebugger 28 | */ 29 | 30 | #import "AppDelegate.h" 31 | #import 32 | 33 | @interface AppDelegate () 34 | 35 | @end 36 | 37 | @implementation AppDelegate 38 | 39 | 40 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 41 | // Override point for customization after application launch. 42 | 43 | // #import 44 | // dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 45 | // for (int i = 0; i < 50; i++) { 46 | // [NSThread sleepForTimeInterval:1]; 47 | // //自定义的 48 | // os_log_t myLog = os_log_create([NSBundle.mainBundle.bundleIdentifier cStringUsingEncoding:NSASCIIStringEncoding], "myDebug"); 49 | // os_log(myLog, "%@ -- %d", @"gogogoogogogogo1", i); 50 | // //默认的 51 | // os_log(OS_LOG_DEFAULT, "%@ -- %d", @"gogogoogogogogo1", i); 52 | // os_log_info(myLog, "%@ -- %d", @"gogogoogogogogo2", i); 53 | // os_log_debug(myLog, "%@ -- %d", @"gogogoogogogogo3", i); 54 | // os_log_error(myLog, "%@ -- %d", @"gogogoogogogogo4", i); 55 | // NSLog(@"Hi %d", i);//CocoaDebug只能监听NSLog和printf,其他的监听不到 56 | // } 57 | // }); 58 | return YES; 59 | } 60 | 61 | - (void)customCocoaDebug { 62 | // pod 'CocoaDebug', '~> 1.7.2', :configurations => ['Debug'] 63 | 64 | //--- If want to custom CocoaDebug settings --- 65 | // CocoaDebug.serverURL = @"google.com"; 66 | // CocoaDebug.ignoredURLs = @[@"aaa.com", @"bbb.com"]; 67 | // CocoaDebug.onlyURLs = @[@"ccc.com", @"ddd.com"]; 68 | // CocoaDebug.ignoredPrefixLogs = @[@"aaa", @"bbb"]; 69 | // CocoaDebug.onlyPrefixLogs = @[@"ccc", @"ddd"]; 70 | // CocoaDebug.logMaxCount = 1000; 71 | // CocoaDebug.emailToRecipients = @[@"aaa@gmail.com", @"bbb@gmail.com"]; 72 | // CocoaDebug.emailCcRecipients = @[@"ccc@gmail.com", @"ddd@gmail.com"]; 73 | // CocoaDebug.mainColor = @"#fd9727"; 74 | // CocoaDebug.additionalViewController = [TestViewController new]; 75 | 76 | //Deprecated! If want to support protobuf, check branch: origin/protobuf_support 77 | //--- If use Google's Protocol buffers --- 78 | // CocoaDebug.protobufTransferMap = @{ 79 | // @"your_api_keywords_1": @[@"your_protobuf_className_1"], 80 | // @"your_api_keywords_2": @[@"your_protobuf_className_2"], 81 | // @"your_api_keywords_3": @[@"your_protobuf_className_3"] 82 | // }; 83 | 84 | //--- If want to manual enable App logs (Take effect the next time when app starts) --- 85 | // CocoaDebugSettings.shared.enableLogMonitoring = YES; 86 | } 87 | 88 | 89 | - (void)applicationWillResignActive:(UIApplication *)application { 90 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 91 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 92 | } 93 | 94 | 95 | - (void)applicationDidEnterBackground:(UIApplication *)application { 96 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 97 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 98 | } 99 | 100 | 101 | - (void)applicationWillEnterForeground:(UIApplication *)application { 102 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 103 | } 104 | 105 | 106 | - (void)applicationDidBecomeActive:(UIApplication *)application { 107 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 108 | } 109 | 110 | 111 | - (void)applicationWillTerminate:(UIApplication *)application { 112 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 113 | } 114 | 115 | 116 | @end 117 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/DeviceNetworkManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceNetworkManager.m 3 | // 此类用来检测服务器是否可达 4 | 5 | #import "DeviceNetworkManager.h" 6 | #import 7 | #import "User.h" 8 | 9 | NSNotificationName const NetworkUsableDidChangeNotification = @"nNetworkUsableDidChangeNotification"; 10 | NSNotificationName const NetworkUsableItem = @"kNetworkUsableItem"; 11 | 12 | //用户不能使用网络 13 | BOOL UsersCannotUseTheNetwork(AFNetworkReachabilityStatus status, BOOL isLoadOnWiFi) 14 | { 15 | return (AFNetworkReachabilityStatusReachableViaWiFi != status 16 | && AFNetworkReachabilityStatusReachableViaWWAN != status)//没网 17 | || (isLoadOnWiFi && AFNetworkReachabilityStatusReachableViaWiFi != status);//不符合用户设置 18 | } 19 | 20 | @implementation DeviceNetworkManager 21 | { 22 | ReachabilityResult _result; 23 | AFNetworkReachabilityManager *_nrm; 24 | NSURLSessionDataTask *_task; 25 | AFNetworkReachabilityStatus _lastStatus; 26 | } 27 | 28 | - (void)dealloc 29 | { 30 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 31 | } 32 | 33 | - (instancetype)init 34 | { 35 | self = [super init]; 36 | if (self) { 37 | _nrm = [AFNetworkReachabilityManager sharedManager]; 38 | _lastStatus = _nrm.networkReachabilityStatus; 39 | } 40 | return self; 41 | } 42 | 43 | static DeviceNetworkManager *sharedManager = nil; 44 | static dispatch_once_t onceTokenManager; 45 | + (instancetype)sharedManager 46 | { 47 | dispatch_once(&onceTokenManager, ^{ 48 | sharedManager = [[self class] new];//此单例是为了监听网络状态 49 | [[NSNotificationCenter defaultCenter] addObserver:sharedManager selector:@selector(reachabilityStatusChanged:) name:AFNetworkingReachabilityDidChangeNotification object:nil]; 50 | [[NSNotificationCenter defaultCenter] addObserver:sharedManager selector:@selector(loadOnWiFiSwitch) name:LoadOnWiFiSwitchNotification object:nil]; 51 | }); 52 | return sharedManager; 53 | } 54 | 55 | static AFHTTPSessionManager *_httpManager = nil; 56 | static dispatch_once_t onceToken; 57 | + (void)initHttpManager 58 | { 59 | dispatch_once(&onceToken, ^{ 60 | _httpManager = [AFHTTPSessionManager manager]; 61 | _httpManager.requestSerializer.timeoutInterval = 5; 62 | _httpManager.operationQueue.maxConcurrentOperationCount = 4; 63 | }); 64 | } 65 | 66 | - (void)loadOnWiFiSwitch 67 | { 68 | if (UsersCannotUseTheNetwork(_nrm.networkReachabilityStatus, User.currentUser.loadOnWiFi)) {//用户不能使用网络 69 | [[NSNotificationCenter defaultCenter] postNotificationName:NetworkUsableDidChangeNotification object:nil userInfo:@{NetworkUsableItem: @NO}]; 70 | } else { 71 | [self deviceReachability:^(BOOL isReachable) { 72 | NSLog(@"%@", isReachable? @"可达": @"不可达"); 73 | if (isReachable) { 74 | [[NSNotificationCenter defaultCenter] postNotificationName:NetworkUsableDidChangeNotification object:nil userInfo:@{NetworkUsableItem: @YES}]; 75 | } 76 | }]; 77 | } 78 | } 79 | 80 | - (void)reachabilityStatusChanged:(NSNotification *)noti 81 | { 82 | AFNetworkReachabilityStatus status = [noti.userInfo[AFNetworkingReachabilityNotificationStatusItem] integerValue]; 83 | if ((AFNetworkReachabilityStatusReachableViaWWAN == status || AFNetworkReachabilityStatusNotReachable == status) && AFNetworkReachabilityStatusReachableViaWiFi == _lastStatus) {//从WiFi变成4G网络或无网络 84 | 85 | } else if (UsersCannotUseTheNetwork(status, User.currentUser.loadOnWiFi)) {//用户设置了只在WiFi是上传或下载 86 | [[NSNotificationCenter defaultCenter] postNotificationName:NetworkUsableDidChangeNotification object:nil userInfo:@{NetworkUsableItem: @NO}]; 87 | } else { 88 | [self deviceReachability:^(BOOL isReachable) { 89 | NSLog(@"%@", isReachable? @"可达": @"不可达"); 90 | if (isReachable) { 91 | [[NSNotificationCenter defaultCenter] postNotificationName:NetworkUsableDidChangeNotification object:nil userInfo:@{NetworkUsableItem: @YES}]; 92 | } 93 | }]; 94 | } 95 | _lastStatus = status; 96 | return; 97 | 98 | if (UsersCannotUseTheNetwork([noti.userInfo[AFNetworkingReachabilityNotificationStatusItem] integerValue], User.currentUser.loadOnWiFi)) {//用户不能使用网络 99 | [[NSNotificationCenter defaultCenter] postNotificationName:NetworkUsableDidChangeNotification object:nil userInfo:@{NetworkUsableItem: @NO}]; 100 | } else { 101 | __weak typeof(self) this = self; 102 | [self deviceReachability:^(BOOL isReachable) { 103 | if (isReachable) { 104 | [[NSNotificationCenter defaultCenter] postNotificationName:NetworkUsableDidChangeNotification object:nil userInfo:@{NetworkUsableItem: @YES}]; 105 | } 106 | }]; 107 | } 108 | } 109 | 110 | - (void)deviceReachability:(ReachabilityResult)result 111 | { 112 | if (nil != _task) { 113 | [_task cancel]; 114 | } 115 | 116 | if (_nrm.isReachable) { 117 | [DeviceNetworkManager initHttpManager]; 118 | _result = [result copy]; 119 | [self checkReachability]; 120 | } else { 121 | if (result) result(NO); 122 | return; 123 | } 124 | } 125 | 126 | //有可能是公网IP 127 | - (void)checkReachability 128 | { 129 | NSString *url = @"http://www.qq.com";//检测是否连接到互联网的API,比如:你官网的URL 130 | 131 | __weak typeof(self) this = self; 132 | _task = [_httpManager POST:url parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { 133 | _result(YES) 134 | } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { 135 | _result(NO) 136 | if (self == sharedManager) [self logout];//是单例发起的请求就退出,其它的不退出 137 | }]; 138 | } 139 | 140 | - (BOOL)cancel 141 | { 142 | if (nil != _task || _task.state == NSURLSessionTaskStateRunning) { 143 | [_task cancel]; 144 | _task = nil; 145 | } 146 | return NO; 147 | } 148 | 149 | - (void)logout 150 | { 151 | //停止一切 152 | User.currentUser = nil; 153 | [[NSNotificationCenter defaultCenter] postNotificationName:UserLogoutNotification object:nil];//通知退出登录 154 | 155 | onceTokenManager = 0; 156 | sharedManager = nil; 157 | } 158 | 159 | @end 160 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/User.mm: -------------------------------------------------------------------------------- 1 | // 2 | // User.mm 3 | // 本类用'WCDB'的'WCTDatabase'操作数据库 4 | 5 | #import "User.h" 6 | #import "User+WCTTableCoding.h" 7 | 8 | float const LowBatteryValue = 0.20000f;//20% 9 | float const LowBatteryMustStopValue = 0.100000f;//10% 10 | NSNotificationName const UserLogoutNotification = @"nUserLogout"; 11 | NSNotificationName const LoadOnWiFiSwitchNotification = @"nLoadOnWiFiSwitch"; 12 | 13 | static NSString *const UserTableNameKey = @"User"; 14 | static NSString *const UserDBFileKey = @"users.sqlite"; 15 | 16 | NS_INLINE 17 | WCTDatabase * UserDatabase() 18 | { 19 | NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:UserDBFileKey]; 20 | return [[WCTDatabase alloc] initWithPath:path]; 21 | } 22 | 23 | @interface User () 24 | @property (nonatomic, assign) int Id;//set方法WCDB库会用到 25 | @end; 26 | 27 | @implementation User 28 | 29 | //使用WCDB_IMPLEMENTATIO宏在类文件定义绑定到数据库表的类 30 | WCDB_IMPLEMENTATION(User) 31 | //使用WCDB_SYNTHESIZE宏在类文件定义需要绑定到数据库表的字段 32 | WCDB_SYNTHESIZE(User, Id) 33 | WCDB_SYNTHESIZE(User, mac) 34 | WCDB_SYNTHESIZE(User, account) 35 | WCDB_SYNTHESIZE(User, password) 36 | WCDB_SYNTHESIZE(User, lastLoginTime) 37 | WCDB_SYNTHESIZE(User, rememberMe) 38 | WCDB_SYNTHESIZE(User, loadOnWiFi) 39 | WCDB_SYNTHESIZE(User, patternLockOn) 40 | WCDB_SYNTHESIZE(User, patternPassword) 41 | WCDB_SYNTHESIZE(User, secondsOfLockDelayed) 42 | WCDB_SYNTHESIZE(User, numberOfRemainingDrawing) 43 | WCDB_SYNTHESIZE(User, autoBackupAlbum) 44 | WCDB_SYNTHESIZE(User, stopBackupAlbumWhenLowBattery) 45 | WCDB_SYNTHESIZE(User, backupPhotos) 46 | WCDB_SYNTHESIZE(User, backupVideos) 47 | WCDB_SYNTHESIZE(User, totalBackup) 48 | WCDB_SYNTHESIZE(User, completedBackup) 49 | WCDB_SYNTHESIZE(User, isPauseAllDownload) 50 | WCDB_SYNTHESIZE(User, isPauseAllUpload) 51 | WCDB_SYNTHESIZE(User, notFirstAutoBackupAlbum) 52 | //主键自增 53 | WCDB_PRIMARY_AUTO_INCREMENT(User, Id) 54 | //索引, 降序 55 | WCDB_INDEX_DESC(User, "_index", lastLoginTime) 56 | //多字段唯一约束 57 | WCDB_MULTI_UNIQUE(User, "_mac_account", mac) 58 | WCDB_MULTI_UNIQUE(User, "_mac_account", account) 59 | 60 | static User *_currentUser; 61 | + (User *)currentUser 62 | { 63 | return _currentUser; 64 | } 65 | 66 | + (void)setCurrentUser:(User *)currentUser 67 | { 68 | _currentUser = currentUser; 69 | } 70 | 71 | + (BOOL)createTable 72 | { 73 | return [UserDatabase() createTableAndIndexesOfName:UserTableNameKey withClass:self.class]; 74 | } 75 | 76 | + (NSArray *)usersForMac:(NSString *)mac 77 | { 78 | return [UserDatabase() getObjectsOfClass:self.class fromTable:UserTableNameKey where:self.mac == mac orderBy:User.lastLoginTime.order(WCTOrderedDescending)]; 79 | } 80 | 81 | + (instancetype)userForMac:(NSString *)mac account:(NSString *)account 82 | { 83 | return [UserDatabase() getOneObjectOfClass:self.class fromTable:UserTableNameKey where:self.mac == mac && self.account == account]; 84 | } 85 | 86 | + (instancetype)lastLoginUserForMac:(NSString *)mac 87 | { 88 | return [UserDatabase() getOneObjectOfClass:self.class fromTable:UserTableNameKey where:self.mac == mac orderBy:User.lastLoginTime.order(WCTOrderedDescending)]; 89 | } 90 | 91 | + (nullable NSArray *)modelPropertyBlacklist 92 | { 93 | return @[NSStringFromSelector(@selector(Id)), NSStringFromSelector(@selector(sessionId))]; 94 | } 95 | 96 | - (instancetype)init 97 | { 98 | self = [super init]; 99 | if (self) { 100 | _Id = -1; 101 | _loadOnWiFi = YES; 102 | _stopBackupAlbumWhenLowBattery = YES; 103 | } 104 | return self; 105 | } 106 | 107 | - (void)dealloc 108 | { 109 | NSLog(@"User -- 释放"); 110 | } 111 | 112 | - (BOOL)updateToLocal 113 | { 114 | WCTDatabase *database = UserDatabase(); 115 | if (_Id < 0) { 116 | //这个是自增插入要做的事 117 | self.isAutoIncrement = YES; 118 | BOOL result = [database insertObject:self into:UserTableNameKey]; 119 | _Id = self.lastInsertedRowID;//获取最新的id 120 | self.isAutoIncrement = NO; 121 | return result; 122 | } 123 | 124 | // return [database insertOrReplaceObject:self into:UserTableNameKey]; 125 | return [database updateRowsInTable:UserTableNameKey onProperties:self.class.AllProperties withObject:self where:self.class.Id == _Id];//'=='是被重载过的运算符 126 | } 127 | 128 | - (BOOL)updateBackupCount 129 | { 130 | return [UserDatabase() updateRowsInTable:UserTableNameKey onProperties:{self.class.completedBackup, self.class.totalBackup} withObject:self where:self.class.Id == _Id]; 131 | } 132 | 133 | - (void)encodeWithCoder:(NSCoder *)aCoder 134 | { 135 | /* 136 | [aCoder encodeObject:_mac forKey:NSStringFromSelector(@selector(mac))]; 137 | [aCoder encodeObject:_account forKey:NSStringFromSelector(@selector(account))]; 138 | [aCoder encodeObject:_password forKey:NSStringFromSelector(@selector(password))]; 139 | [aCoder encodeDouble:_lastLoginTime forKey:NSStringFromSelector(@selector(lastLoginTime))]; 140 | [aCoder encodeBool:_rememberMe forKey:NSStringFromSelector(@selector(rememberMe))]; 141 | [aCoder encodeBool:_loadOnWiFi forKey:NSStringFromSelector(@selector(loadOnWiFi))]; 142 | [aCoder encodeBool:_patternLockOn forKey:NSStringFromSelector(@selector(patternLockOn))]; 143 | [aCoder encodeInteger:_patternPassword forKey:NSStringFromSelector(@selector(patternPassword))]; 144 | [aCoder encodeInteger:_secondsOfLockDelayed forKey:NSStringFromSelector(@selector(secondsOfLockDelayed))]; 145 | [aCoder encodeInt:_numberOfRemainingDrawing forKey:NSStringFromSelector(@selector(numberOfRemainingDrawing))]; 146 | [aCoder encodeBool:_autoBackupAlbum forKey:NSStringFromSelector(@selector(autoBackupAlbum))]; 147 | [aCoder encodeBool:_stopBackupAlbumWhenLowBattery forKey:NSStringFromSelector(@selector(stopBackupAlbumWhenLowBattery))]; 148 | [aCoder encodeBool:_backupPhotos forKey:NSStringFromSelector(@selector(backupPhotos))]; 149 | [aCoder encodeBool:_backupVideos forKey:NSStringFromSelector(@selector(backupVideos))]; 150 | */ 151 | // [self yy_modelEncodeWithCoder:aCoder]; 152 | } 153 | 154 | - (instancetype)initWithCoder:(NSCoder *)aDecoder 155 | { 156 | self = [super init]; 157 | if (self) { 158 | // [self yy_modelInitWithCoder:aDecoder]; 159 | } 160 | return self; 161 | } 162 | 163 | - (BOOL)isEqual:(User *)other 164 | { 165 | if (other == self) { 166 | return YES; 167 | }/* else if (![super isEqual:other]) { 168 | return NO; 169 | }*/ else if (![other isMemberOfClass:[self class]]) { 170 | return NO; 171 | } else { 172 | if (_Id > 0 && other->_Id > 0) { 173 | return _Id == other->_Id; 174 | } 175 | return [_mac isEqual:other->_mac] && [_account isEqual:other->_account]; 176 | } 177 | } 178 | 179 | - (NSUInteger)hash 180 | { 181 | if (_Id > 0) { 182 | return _Id; 183 | } 184 | return _mac.hash ^ _account.hash; 185 | } 186 | 187 | @end 188 | -------------------------------------------------------------------------------- /Template/Share.m: -------------------------------------------------------------------------------- 1 | // 2 | // Share.m 3 | // 4 | // 5 | // Created by mxm on 2019/1/2. 6 | // Copyright © 2019 mxm. All rights reserved. 7 | // 8 | 9 | //#import 10 | //#import 11 | //#import 12 | 13 | #import 14 | #import 15 | 16 | @interface Share: NSObject /// 17 | 18 | @end 19 | 20 | @implementation Share 21 | 22 | //各种appId注册 23 | + (void)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 24 | { 25 | // [[FBSDKApplicationDelegate sharedInstance] application:app didFinishLaunchingWithOptions:launchOptions]; 26 | // [[Twitter sharedInstance] startWithConsumerKey:@"" consumerSecret:@""]; 27 | } 28 | 29 | //各种分享的回调 30 | + (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options 31 | { 32 | /* 33 | if ([url.scheme hasPrefix:@"fb"]) { 34 | return [[FBSDKApplicationDelegate sharedInstance] application:app openURL:url sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]; 35 | } else if ([url.scheme hasPrefix:@"twitterkit"]) { 36 | return [[Twitter sharedInstance] application:app openURL:url options:options]; 37 | }*/ 38 | 39 | return NO; 40 | } 41 | 42 | #pragma mark - UIActivityViewController 43 | /* 44 | Social ServiceType 45 | //------------------iOS12有效---------------------- 46 | //推特 47 | @"com.apple.share.Twitter.post"; 48 | //脸书 49 | @"com.apple.share.Facebook.post"; 50 | 51 | //------------------iOS12有效,但是不能直接使用------- 52 | //QQ 53 | @"com.tencent.mqq.ShareExtension"; 54 | //微信 55 | @"com.tencent.xin.sharetimeline"; 56 | 57 | //------------------未验证------------------------- 58 | //Instagram 59 | @"com.burbn.instagram.shareextension"; 60 | //新浪微博 61 | @"com.apple.share.SinaWeibo.post"; 62 | //支付宝 63 | @"com.alipay.iphoneclient.ExtensionSchemeShare"; 64 | //备忘录 65 | @"com.apple.mobilenotes.SharingExtension"; 66 | //提醒事项 67 | @"com.apple.reminders.RemindersEditorExtension"; 68 | //iCloud 69 | @"com.apple.mobileslideshow.StreamShareService"; 70 | 71 | com.taobao.taobao4iphone.ShareExtension //淘宝 72 | com.apple.share.Flickr.post //Flickr 73 | com.laiwang.DingTalk.ShareExtension //钉钉 74 | com.alipay.iphoneclient.ExtensionSchemeShare //支付宝 75 | com.apple.Health.HealthShareExtension //应该是健康管理 76 | */ 77 | 78 | //使用UIActivityViewController,只要含有ShareExtension的App都可使用 79 | + (void)share:(NSString *)URLString showFrom:(UIViewController *)vc 80 | {//此方法使用的是iOS自带的分享控件,但是要预先填好数据 81 | 82 | /* 83 | 1、 有图片时,url 和 title无效不显示 . 84 | NSArray *activityItems = @[shareImg,shareURL,shareTitle]; 85 | 86 | 2、可以添加多张图片,默认显示第一张,可以滑动查看图片,如: 87 | NSArray *activityItems = @[shareImg,shareImg1,shareURL,shareTitle]; 88 | 89 | 3、有url 和 title 时,优先显示url,不显示title,如: 90 | NSArray *activityItems = @[shareTitle,shareURL]; 91 | 92 | 4、只有文字时,才显示文字,如: 93 | NSArray *activityItems = @[shareTitle]; 94 | */ 95 | 96 | NSString *text = @"测试一下"; 97 | NSURL *url = [NSURL URLWithString:URLString]; 98 | 99 | NSArray *items = @[text, url]; 100 | UIActivityViewController *avc = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; 101 | avc.excludedActivityTypes = @[ 102 | UIActivityTypeAirDrop, 103 | UIActivityTypeAddToReadingList 104 | ]; 105 | //QQ和微信可以使用这种方式 106 | [vc presentViewController:avc animated:YES completion:nil]; 107 | } 108 | 109 | //------------------------Facebook------------------------- 110 | #pragma mark - Facebook 111 | + (void)facebookShare:(NSString *)URLString showFrom:(UIViewController *)vc 112 | {//此方法使用的是自定义分享控件,点击分享后再调用此方法,然后填写数据 113 | //iOS自带方式 114 | static NSString * const type = @"com.apple.share.Facebook.post";//这个有效 115 | NSLog(@"Facebook分享:%@", [SLComposeViewController isAvailableForServiceType:type]?@"yes":@"no");//可以检测App是否安装 116 | //未安装App无法使用此方法 117 | SLComposeViewController *cvc = [SLComposeViewController composeViewControllerForServiceType:type]; 118 | [cvc addURL:[NSURL URLWithString:URLString]]; 119 | // cvc addImage:<#(UIImage *)#> 120 | cvc.completionHandler = ^(SLComposeViewControllerResult result){ 121 | if (result == SLComposeViewControllerResultCancelled) { 122 | NSLog(@"点击了取消"); 123 | } else { 124 | NSLog(@"点击了发送"); 125 | } 126 | }; 127 | //QQ和微信不允许使用这种方式 128 | [vc presentViewController:cvc animated:YES completion:nil]; 129 | 130 | //使用Facebook官方SDK 131 | // FBSDKShareLinkContent *content = [FBSDKShareLinkContent new]; 132 | // content.contentURL = [NSURL URLWithString:URLString]; 133 | //预定义 话题标签 134 | // content.hashtag = [FBSDKHashtag hashtagWithString:@"#Image"]; 135 | //预定义 引文 136 | // content.quote = @"快来围观啊"; 137 | //方式一 138 | // [FBSDKShareDialog showFromViewController:vc withContent:content delegate:(id)self]; 139 | 140 | //方式二: 141 | // FBSDKShareDialog *dialog = [FBSDKShareDialog new]; 142 | // dialog.fromViewController = vc; 143 | // dialog.shareContent = content; 144 | // dialog.mode = FBSDKShareDialogModeShareSheet; 145 | // [dialog show]; 146 | } 147 | 148 | //使用Facebook官方SDK的回调 149 | /* 150 | + (void)sharer:(id)sharer didCompleteWithResults:(NSDictionary *)results 151 | { 152 | } 153 | 154 | + (void)sharer:(id)sharer didFailWithError:(NSError *)error 155 | { 156 | } 157 | 158 | + (void)sharerDidCancel:(id)sharer 159 | { 160 | }*/ 161 | 162 | //------------------------Twitter------------------------- 163 | #pragma mark - Twitter 164 | + (void)twitterShare:(NSString *)URLString showFrom:(UIViewController *)vc 165 | {//此方法使用的是自定义分享控件,点击分享后再调用此方法,然后填写数据 166 | static NSString * const type = @"com.apple.share.Twitter.post";//这个有效 167 | NSLog(@"Twitter分享:%@", [SLComposeViewController isAvailableForServiceType:type]?@"yes":@"no");//可以检测App是否安装 168 | //未安装App无法使用此方法 169 | SLComposeViewController *cvc = [SLComposeViewController composeViewControllerForServiceType:type]; 170 | [cvc setInitialText:@"测试"]; 171 | [cvc addURL:[NSURL URLWithString:URLString]]; 172 | cvc.completionHandler = ^(SLComposeViewControllerResult result){ 173 | if (result == SLComposeViewControllerResultCancelled) { 174 | NSLog(@"点击了取消"); 175 | } else { 176 | NSLog(@"点击了发送"); 177 | } 178 | }; 179 | [vc presentViewController:cvc animated:YES completion:nil]; 180 | 181 | //使用Twitter官方SDK 182 | // TWTRComposer *composer = [TWTRComposer new]; 183 | // [composer setText:@"测试一下"]; 184 | // [composer setURL:[NSURL URLWithString:URLString]]; 185 | // [composer showFromViewController:vc completion:^(TWTRComposerResult result) { 186 | // if (result == TWTRComposerResultCancelled) { 187 | // NSLog(@"Tweet composition cancelled"); 188 | // } else { 189 | // [SVProgressHUD showSuccessWithStatus:@"Share success"]; 190 | // [SVProgressHUD dismissWithDelay:3]; 191 | // } 192 | // }]; 193 | } 194 | 195 | //-----------------------Email分享--------------------------- 196 | #pragma mark - Email 197 | + (void)emailShare:(NSString *)URLString showFrom:(UIViewController *)vc 198 | { 199 | MFMailComposeViewController *mcvc = [[MFMailComposeViewController alloc]init]; 200 | mcvc.mailComposeDelegate = (id)self; 201 | [mcvc setSubject:@"测试一下"]; 202 | [mcvc setMessageBody:URLString isHTML:NO]; 203 | [vc presentViewController:mcvc animated:YES completion:nil]; 204 | } 205 | 206 | + (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error 207 | { 208 | NSLog(@"Result : %ld",(long)result); 209 | if (result == MFMailComposeResultSent) { 210 | } 211 | if (error) { 212 | NSLog(@"Error : %@", error); 213 | } 214 | [controller dismissViewControllerAnimated:YES completion:nil]; 215 | } 216 | 217 | @end 218 | 219 | /* 220 | [UIImage imageNamed:@"xxx.bundle/xxx"]; 221 | 或 222 | //静态库获取内部的图片或xib 223 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 224 | bundle = [UIImage imageNamed:bundle inBundle:bundle compatibleWithTraitCollection:nil]; 225 | 226 | NSBundle *bundle = [NSBundle bundleForClass:[FrameworkViewController class]];FrameworkViewController *frameworkVC = [[FrameworkViewController alloc] initWithNibName:@"FrameworkViewController" bundle:bundle]; 227 | */ 228 | -------------------------------------------------------------------------------- /Template/DownloadUploadBackupCommon/FileTask.mm: -------------------------------------------------------------------------------- 1 | // 2 | // FileTask.mm 3 | // 本类用'WCDB'的'WCTTable'操作数据库 4 | // insertOrReplaceObject 1.0.5、1.0.6有bug 5 | 6 | #import "FileTask.h" 7 | #import "FileTask+WCTTableCoding.h" 8 | #import "User.h" 9 | 10 | static NSString *const FileTaskTableNameKey = @"FileTask"; 11 | static NSString *const FileTaskDBFileKey = @"filetask.sqlite"; 12 | 13 | NS_INLINE 14 | WCTDatabase * FileTaskDatabase() 15 | { 16 | static NSString *path = nil; 17 | static dispatch_once_t onceToken; 18 | dispatch_once(&onceToken, ^{ 19 | path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:FileTaskDBFileKey]; 20 | }); 21 | return [[WCTDatabase alloc] initWithPath:path]; 22 | } 23 | 24 | NS_INLINE 25 | WCTTable * FileTaskTable() 26 | { 27 | return [FileTaskDatabase() getTableOfName:FileTaskTableNameKey withClass:FileTask.class]; 28 | } 29 | 30 | @interface FileTask () 31 | 32 | @property (nonatomic, assign) NSUInteger Id; 33 | 34 | @end 35 | 36 | @implementation FileTask 37 | { 38 | NSString *_createTimeFormatString; //创建时间格式化的字符串 39 | } 40 | 41 | WCDB_IMPLEMENTATION(FileTask) 42 | 43 | WCDB_SYNTHESIZE(FileTask, Id) 44 | WCDB_SYNTHESIZE(FileTask, mac) 45 | WCDB_SYNTHESIZE(FileTask, userId) 46 | WCDB_SYNTHESIZE(FileTask, fileName) 47 | WCDB_SYNTHESIZE(FileTask, fileExt) 48 | WCDB_SYNTHESIZE(FileTask, mediaType) 49 | WCDB_SYNTHESIZE(FileTask, state) 50 | WCDB_SYNTHESIZE(FileTask, size) 51 | WCDB_SYNTHESIZE(FileTask, completedSize) 52 | WCDB_SYNTHESIZE(FileTask, type) 53 | WCDB_SYNTHESIZE(FileTask, serverPath) 54 | WCDB_SYNTHESIZE(FileTask, createTime) 55 | WCDB_SYNTHESIZE(FileTask, localPath) 56 | WCDB_SYNTHESIZE(FileTask, currentFragment) 57 | WCDB_SYNTHESIZE(FileTask, totalFragment) 58 | WCDB_SYNTHESIZE(FileTask, assetLocalIdentifier) 59 | WCDB_SYNTHESIZE(FileTask, filetype) 60 | WCDB_SYNTHESIZE(FileTask, resumeDataName) 61 | 62 | WCDB_PRIMARY_AUTO_INCREMENT(FileTask, Id) 63 | //WCDB_PRIMARY_ASC_AUTO_INCREMENT(FileTask, Id) 64 | 65 | + (BOOL)createTable 66 | { 67 | return [FileTaskDatabase() createTableAndIndexesOfName:FileTaskTableNameKey withClass:self]; 68 | } 69 | 70 | + (NSInteger)fileTaskMaxId 71 | { 72 | return [[FileTaskTable() getOneValueOnResult:self.Id.max()] integerValue]; 73 | } 74 | 75 | + (NSArray *)progressFileTasksForUser:(User *)user taskType:(FileTaskType)type 76 | { 77 | return [FileTaskTable() getObjectsWhere:(self.mac == user.mac) && (self.userId == user.Id) && (self.type == type) && (self.state != FileTaskStatusCompleted && self.state != FileTaskStatusError) orderBy:self.Id.order(WCTOrderedAscending)]; 78 | } 79 | 80 | + (NSArray *)progressFileTasksForUser:(User *)user taskType:(FileTaskType)type offset:(NSUInteger)offset 81 | { 82 | return [FileTaskTable() getObjectsWhere:(self.mac == user.mac) && (self.userId == user.Id) && (self.type == type) && (self.state != FileTaskStatusCompleted && self.state != FileTaskStatusError) orderBy:self.Id.order(WCTOrderedAscending) offset:offset]; 83 | } 84 | 85 | + (NSArray *)progressFileTasksForUser:(User *)user taskType:(FileTaskType)type idGreaterThan:(NSInteger)Id 86 | { 87 | return [FileTaskTable() getObjectsWhere:(self.mac == user.mac) && (self.userId == user.Id) && (self.type == type) && (self.state != FileTaskStatusCompleted && self.state != FileTaskStatusError) && (self.Id > Id) orderBy:self.Id.order(WCTOrderedAscending)]; 88 | } 89 | 90 | + (NSArray *)successFileTasksForUser:(User *)user taskType:(FileTaskType)type 91 | { 92 | return [FileTaskTable() getObjectsWhere:(self.mac == user.mac) && (self.userId == user.Id) && (self.type == type) && (self.state == FileTaskStatusCompleted) orderBy:self.Id.order(WCTOrderedAscending)]; 93 | } 94 | 95 | + (NSArray *)failureFileTasksForUser:(User *)user taskType:(FileTaskType)type 96 | { 97 | return [FileTaskTable() getObjectsWhere:(self.mac == user.mac) && (self.userId == user.Id) && (self.type == type) && (self.state == FileTaskStatusError) orderBy:self.Id.order(WCTOrderedAscending)]; 98 | } 99 | 100 | + (BOOL)addFileTasks:(NSArray *)fileTasks 101 | { 102 | return [FileTaskTable() insertObjects:fileTasks]; 103 | } 104 | 105 | + (BOOL)isExistsFileTaskForUser:(User *)user serverPath:(NSString *)serverPath fileTaskType:(FileTaskType)type 106 | { 107 | return [[FileTaskTable() getOneValueOnResult:self.AnyProperty.count() where:self.mac == user.mac && self.userId == user.Id && self.type == type && self.serverPath == serverPath] intValue] > 0; 108 | } 109 | 110 | + (BOOL)deleteAllFileTasksForUser:(User *)user forType:(FileTaskType)type 111 | { 112 | return [FileTaskTable() deleteObjectsWhere:self.mac == user.mac && self.userId == user.Id && self.type == type]; 113 | } 114 | 115 | + (BOOL)deleteFileTask:(FileTask *)ftask 116 | { 117 | return [FileTaskTable() deleteObjectsWhere:self.Id == ftask.Id]; 118 | } 119 | 120 | //(NSArray *)ftasks 121 | //+ (BOOL)deleteFileTaskWithIDs:(NSIndexSet *)indexSet 122 | //{ 123 | // return [FileTaskTable() deleteObjectsWhere:self.Id.in(indexSet)]; 124 | //} 125 | 126 | + (BOOL)deleteFileTaskWithIDs:(NSArray *)ids 127 | { 128 | return [FileTaskTable() deleteObjectsWhere:self.Id.in(ids)]; 129 | } 130 | 131 | + (BOOL)updateFileTasks:(NSArray *)fileTasks 132 | { 133 | return [FileTaskTable() insertOrReplaceObjects:fileTasks onProperties:{self.state, self.size, self.completedSize, self.currentFragment, self.totalFragment, self.localPath, self.resumeDataName}]; 134 | } 135 | 136 | - (instancetype)init 137 | { 138 | self = [super init]; 139 | if (self) { 140 | _Id = 0; 141 | _canUpload = YES; 142 | } 143 | return self; 144 | } 145 | 146 | - (instancetype)initForInsert 147 | { 148 | self = [super init]; 149 | if (self) { 150 | _Id = 0; 151 | _canUpload = YES; 152 | self.isAutoIncrement = YES; 153 | } 154 | return self; 155 | } 156 | 157 | - (BOOL)updateToLocal 158 | { 159 | WCTTable *table = FileTaskTable(); 160 | if (0 == _Id) { 161 | //这个是自增插入要做的事 162 | self.isAutoIncrement = YES; 163 | BOOL result = [table insertObject:self]; 164 | _Id = self.lastInsertedRowID;//获取最新的id 165 | self.isAutoIncrement = NO; 166 | return result; 167 | } 168 | // return [table insertOrReplaceObject:self];//1.0.5此方法有bug 169 | return [table updateRowsOnProperties:self.class.AllProperties withObject:self where:self.class.Id == _Id]; 170 | } 171 | 172 | - (BOOL)updateStatusToLocal 173 | { 174 | return [FileTaskTable() updateRowsOnProperties:{self.class.state, self.class.size, self.class.completedSize, self.class.currentFragment, self.class.totalFragment, self.class.localPath, self.class.resumeDataName} withObject:self where:self.class.Id == _Id]; 175 | // return [FileTaskTable() insertOrReplaceObject:self onProperties:{self.class.state, self.class.size, self.class.completedSize}];//这个有bug 176 | } 177 | 178 | - (BOOL)isEqual:(FileTask *)other 179 | { 180 | if (other == self) { 181 | return YES; 182 | }/* else if (![super isEqual:other]) { 183 | return NO; 184 | }*/ else if (![other isMemberOfClass:[self class]]) { 185 | return NO; 186 | } else { 187 | if (_Id > 0 && other->_Id > 0) { 188 | return _Id == other->_Id; 189 | } 190 | return [other->_mac isEqual:_mac] && other->_userId == _userId && [other->_serverPath isEqual:_serverPath]; 191 | } 192 | } 193 | 194 | - (NSUInteger)hash 195 | { 196 | if (_Id > 0) { 197 | return _Id; 198 | } 199 | return _mac.hash ^ _userId ^ _serverPath.hash; 200 | } 201 | 202 | #pragma mark - 计算属性 203 | - (NSString *)speedFormatString 204 | { 205 | return [[NSByteCountFormatter stringFromByteCount:_transmissionSpeed countStyle:NSByteCountFormatterCountStyleBinary] stringByAppendingString:@"/s"]; 206 | } 207 | 208 | - (NSString *)sizeFormatString 209 | { 210 | if (nil == _sizeFormatString) { 211 | _sizeFormatString = [NSByteCountFormatter stringFromByteCount:_size countStyle:NSByteCountFormatterCountStyleBinary]; 212 | } 213 | return _sizeFormatString; 214 | } 215 | 216 | - (NSString *)completedSizeFormatString 217 | { 218 | return [NSByteCountFormatter stringFromByteCount:_completedSize countStyle:NSByteCountFormatterCountStyleBinary]; 219 | } 220 | 221 | - (NSString *)createTimeFormatString 222 | { 223 | if (nil == _createTimeFormatString) { 224 | _createTimeFormatString = [NSDateFormatter localizedStringFromDate:[NSDate dateWithTimeIntervalSince1970:_createTime] dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterShortStyle]; 225 | } 226 | return _createTimeFormatString; 227 | } 228 | 229 | @end 230 | -------------------------------------------------------------------------------- /Template/TableViewTemplate.m: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewTemplate.m 3 | // 4 | // Created by mxm on 2017/5/13. 5 | // Copyright © 2017年 mxm. All rights reserved. 6 | // 7 | // tableView模板使用 8 | 9 | #import 10 | 11 | @interface TableViewTemplate: UIViewController 12 | 13 | @end 14 | 15 | @implementation TableViewTemplate 16 | { 17 | 18 | UITableView *_tableView; 19 | NSMutableArray *_arr; 20 | UIBarButtonItem *_selectAllItem; 21 | } 22 | 23 | - (void)didReceiveMemoryWarning { 24 | [super didReceiveMemoryWarning]; 25 | if (!_tableView.editing) {//不编辑的时候 26 | self.toolbarItems = nil; 27 | _selectAllItem = nil; 28 | } 29 | } 30 | 31 | - (void)viewDidLoad 32 | { 33 | [super viewDidLoad]; 34 | 35 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(pushToAdd)]; 36 | 37 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped]; 38 | _tableView.dataSource = self; 39 | _tableView.delegate = self; 40 | _tableView.sectionFooterHeight = 0; 41 | _tableView.estimatedSectionHeaderHeight = 20; 42 | _tableView.allowsMultipleSelectionDuringEditing = YES;//用系统的多选形式 43 | [self.view addSubview:_tableView]; 44 | 45 | _arr = [NSMutableArray arrayWithObjects:@"老大", @"12312", nil]; 46 | } 47 | 48 | - (void)viewWillDisappear:(BOOL)animated 49 | { 50 | [super viewWillDisappear:animated]; 51 | [self.navigationController setToolbarHidden:YES animated:NO]; 52 | } 53 | 54 | - (void)viewDidDisappear:(BOOL)animated 55 | { 56 | [super viewDidDisappear:animated]; 57 | [self cancelAction]; 58 | } 59 | 60 | - (void)pushToAdd {} 61 | 62 | - (void)showEditing 63 | { 64 | if (_tableView.isEditing) return; 65 | if (nil == self.toolbarItems) { 66 | UIBarButtonItem *deleteItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(deletePrompt)]; 67 | UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];//自动调节间距 68 | _selectAllItem = [[UIBarButtonItem alloc] initWithTitle:@"全选" style:UIBarButtonItemStylePlain target:self action:@selector(selectAllCell)]; 69 | self.toolbarItems = @[_selectAllItem, spaceItem, deleteItem]; 70 | } else { 71 | _selectAllItem.title = @"全选"; 72 | } 73 | 74 | _tableView.editing = YES; 75 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelAction)]; 76 | [self.navigationController setToolbarHidden:NO animated:YES]; 77 | 78 | [_tableView reloadData]; 79 | } 80 | 81 | - (void)cancelAction 82 | { 83 | _tableView.editing = NO; 84 | self.navigationItem.rightBarButtonItem = nil; 85 | [_tableView reloadData]; 86 | 87 | [self.navigationController setToolbarHidden:YES animated:YES]; 88 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(pushToAdd)]; 89 | } 90 | 91 | //删除多个前的提示 92 | - (void)deletePrompt 93 | { 94 | NSUInteger count = _tableView.indexPathsForSelectedRows.count; 95 | if (0 == count) return; 96 | //这里写自定义逻辑 97 | } 98 | 99 | //全选 100 | - (void)selectAllCell 101 | { 102 | NSUInteger count = _arr.count; 103 | if (_tableView.indexPathsForSelectedRows.count != count) {//不是全选 104 | for (int i = 0; i < count; ++i) { 105 | [_tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0] animated:YES scrollPosition:UITableViewScrollPositionNone]; 106 | } 107 | _selectAllItem.title = @"取消全选"; 108 | } else { 109 | [_tableView reloadData]; 110 | _selectAllItem.title = @"全选"; 111 | } 112 | } 113 | 114 | #pragma mark - tableview代理 115 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 116 | { 117 | return _arr.count; 118 | } 119 | 120 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 121 | { 122 | static NSString *identify = @"kIdentify"; 123 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identify]; 124 | if (nil == cell) { 125 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identify]; 126 | } 127 | cell.textLabel.text = _arr[indexPath.row]; 128 | return cell; 129 | } 130 | 131 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 132 | { 133 | if (!tableView.editing) { 134 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 135 | //这里自定义逻辑 136 | } else { 137 | if (_arr.count == tableView.indexPathsForSelectedRows.count) {//已全选 138 | _selectAllItem.title = @"取消全选"; 139 | } 140 | } 141 | } 142 | 143 | - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath 144 | { 145 | if (!tableView.isEditing) return; 146 | if (_arr.count != tableView.indexPathsForSelectedRows.count) {//没有全选 147 | _selectAllItem.title = @"全选"; 148 | } 149 | } 150 | 151 | #pragma mark - Cell侧滑删除 152 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath 153 | { 154 | return YES; 155 | } 156 | // iOS 11 起: 157 | - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath 158 | { 159 | UIContextualAction *deleteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:@"删除" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) { 160 | [self->_arr removeObjectAtIndex:indexPath.row]; 161 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; 162 | completionHandler(true); 163 | }]; 164 | deleteAction.backgroundColor = UIColor.blueColor; 165 | 166 | UIContextualAction *renameAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"重命名" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) { 167 | // 168 | }]; 169 | renameAction.backgroundColor = UIColor.redColor; 170 | 171 | return [UISwipeActionsConfiguration configurationWithActions:@[deleteAction, renameAction]]; 172 | } 173 | 174 | // iOS 8 起: 175 | - (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath 176 | { 177 | UITableViewRowAction *deleteAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"删除" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) { 178 | [self->_arr removeObjectAtIndex:indexPath.row]; 179 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; 180 | }]; 181 | UITableViewRowAction *renameAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:@"重命名" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) { 182 | // 183 | }]; 184 | 185 | // You can add more actions as needed 186 | // Return the array of row actions 187 | return @[deleteAction, renameAction]; 188 | } 189 | 190 | // iOS 8 之前: 191 | - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath 192 | { 193 | return UITableViewCellEditingStyleDelete; 194 | } 195 | 196 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath 197 | { 198 | [_arr removeObjectAtIndex:indexPath.row];//删除数据 199 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];//删除行cell 200 | } 201 | 202 | #pragma mark - Cell长按触发 203 | //这下面3这个方法少一个都不行,长按会触发 204 | /* 205 | 本来这3个方法是用来给UITableViewCell弹出Menu的,这里被我禁用了, 206 | 只是借用了一下长按触发事件 207 | */ 208 | - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath 209 | { 210 | [self showEditing]; 211 | [tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone]; 212 | return NO; 213 | } 214 | - (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender 215 | { 216 | return NO; 217 | } 218 | - (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender {} 219 | 220 | @end 221 | -------------------------------------------------------------------------------- /Template/TemplateUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // LocalAuthentication.m 3 | // 4 | // 5 | // Created by mxm on 2019/2/10. 6 | // Copyright © 2019 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface TemplateUtils : NSObject 12 | @end 13 | 14 | @implementation TemplateUtils 15 | 16 | static const int kTimeoutInterval = 3; 17 | NS_INLINE 18 | dispatch_source_t fetchGCDTimer(void) { 19 | dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); 20 | /*! 21 | * @param start 22 | * 首次触发时间,单位纳秒。 See dispatch_time() and dispatch_walltime() 23 | * for more information. 我这里用 dispatch_time 计算3秒后的时间 24 | * 25 | * @param interval 26 | * 时间间隔,单位纳秒。 使用 DISPATCH_TIME_FOREVER 表示一次性的timer 27 | * 28 | * @param leeway 29 | * 可偏差时间 30 | */ 31 | dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, kTimeoutInterval * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0.1 * NSEC_PER_SEC); 32 | return timer; 33 | } 34 | 35 | // 使用 GCD timer 的好处是不用考虑 runloop 36 | - (void)gcdTimer 37 | { 38 | dispatch_source_t timer = fetchGCDTimer(); 39 | dispatch_source_set_event_handler(timer, ^() { 40 | // 如果是重复触发 timer 可在这里 停止 timer 41 | // dispatch_source_cancel(timer); 42 | // TODO sometings 43 | }); 44 | 45 | // 启动 timer 46 | dispatch_resume(timer); 47 | // 停止timer 48 | dispatch_source_cancel(timer); 49 | } 50 | 51 | /// 复制到剪切板 52 | + (void)copyToClipboard:(NSString *)text 53 | { 54 | if (!text || text.length == 0) { 55 | return; 56 | } 57 | UIPasteboard.generalPasteboard.string = text; 58 | } 59 | 60 | /// 使用iOS原生类请求 HTTP JSON 61 | + (void)postHttp:(NSURL *)url header:(NSDictionary *)headers body:(NSDictionary *)json completion:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completion 62 | { 63 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30]; 64 | request.HTTPMethod = @"POST"; 65 | for (NSString *key in headers) { 66 | [request setValue:headers[key] forHTTPHeaderField:key]; 67 | } 68 | [request setValue:@"application/json;charset=utf-8" forHTTPHeaderField:@"Content-Type"]; 69 | request.HTTPBody = [NSJSONSerialization dataWithJSONObject:json options:kNilOptions error:NULL]; 70 | NSURLSessionDataTask *task = [NSURLSession.sharedSession dataTaskWithRequest:request completionHandler:completion]; 71 | [task resume]; 72 | } 73 | 74 | /// 从AppStore获取版app最新本号 75 | + (void)fetchNewVersion 76 | { 77 | /* 78 | http 和 https 貌似都可 79 | 全球: 80 | https://itunes.apple.com/lookup?id= 81 | http://itunes.apple.com/lookup?bundleId= 82 | 中国: 83 | https://itunes.apple.com/cn/lookup?id= 84 | http://itunes.apple.com/cn/lookup?bundleId= 85 | */ 86 | [[NSURLSession.sharedSession dataTaskWithURL:[NSURL URLWithString:[@"http://itunes.apple.com/lookup?bundleId=" stringByAppendingString:NSBundle.mainBundle.bundleIdentifier]] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { 87 | if (error) return; 88 | 89 | NSError *err; 90 | NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&err]; 91 | if (err) return; 92 | //dic[@"resultCount"].intValue > 0 93 | // NSLog(@"--: %@", dict); 94 | NSArray *arr = dict[@"results"]; 95 | if (arr.count <= 0) return; 96 | 97 | // NSLog(@"--: %@", dic[@"resultCount"]); 98 | NSDictionary *info = arr[0]; 99 | NSNumber *trackId = info[@"trackId"]; 100 | NSString *version = info[@"version"]; 101 | // 此链接可以跳到AppStore的app下载页 102 | NSString *trackViewUrl = info[@"trackViewUrl"]; 103 | NSLog(@"--: %@", trackId); 104 | NSLog(@"--: %@", version); 105 | }] resume]; 106 | } 107 | 108 | #pragma mark - 使用 NSURLSession.sharedSession 下载文件并获取进度,这样就可以不用自己创建NSURLSession 来设置NSURLSessionDownloadDelegate去获取进度 109 | static NSURLSessionDownloadTask *_task; 110 | static dispatch_source_t _timer; 111 | - (void)downloadFile 112 | { 113 | __weak __typeof(self) weakSelf = self; 114 | // 定时器,定时获取下载进度 115 | _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); 116 | dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC), 0.5 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); 117 | dispatch_source_set_event_handler(_timer, ^() { 118 | __strong __typeof(weakSelf) self = weakSelf; 119 | if (nil == _task || _task.countOfBytesExpectedToReceive == 0) return; 120 | 121 | // 进度 122 | [NSString stringWithFormat:@"%lld%%", _task.countOfBytesReceived * 100 / _task.countOfBytesExpectedToReceive]; 123 | }); 124 | 125 | _task = [NSURLSession.sharedSession downloadTaskWithURL:[NSURL URLWithString:@"file url"] completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) { 126 | // 停止定时器 127 | dispatch_source_cancel(_timer); 128 | __strong __typeof(weakSelf) self = weakSelf; 129 | // 下载完成或出错 130 | }]; 131 | [_task resume]; 132 | // 启动定时器 133 | dispatch_resume(_timer); 134 | } 135 | 136 | @end 137 | 138 | #pragma mark - UITextField 添加 leftView 139 | @interface UITextField (LeftText) 140 | 141 | - (void)setLeftText:(NSString *)text; 142 | 143 | @end 144 | 145 | @implementation UITextField (LeftText) 146 | 147 | - (void)setLeftText:(NSString *)text 148 | { 149 | // 使用 UIButton 可以设置留空白, UILabel不能留空白,因为 UITextField 会重新计算并设置 leftView 的 frame 150 | UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; 151 | btn.userInteractionEnabled = NO; 152 | [btn setTitle:[text stringByAppendingString:@": "] forState:UIControlStateNormal]; 153 | [btn setTitleColor:UIColor.blackColor forState:UIControlStateNormal]; 154 | // 留空白 155 | btn.contentEdgeInsets = UIEdgeInsetsMake(0, 10, 0, 0); 156 | 157 | self.leftView = btn; 158 | // 这个必须设置,否则不会显示 159 | self.leftViewMode = UITextFieldViewModeAlways; 160 | } 161 | 162 | @end 163 | 164 | #pragma mark - UIButton 165 | @interface UIButton (ImageAndTitleSpace) @end 166 | @implementation UIButton (ImageAndTitleSpace) 167 | // 设置 UIButton 图片和文字之间的间隔 168 | - (void)setImageAndTitleSpace:(CGFloat)space 169 | { 170 | // 必须是 imageEdgeInsets 设置间隔,titleEdgeInsets 会导致 UIButtonTypeCustom 类型的文字出现省略。 left 必须是负数。image可能会超出 self.bounds 的左边界,注意设置了切割超出边界属性 171 | self.imageEdgeInsets = UIEdgeInsetsMake(0, -space, 0, 0);// storyboard 和 xib 的属性面板上可以设置 172 | // 这么设置一下,image就不会超出 self.bounds 的左边界了 173 | self.contentEdgeInsets = UIEdgeInsetsMake(0, space, 0, 0); 174 | 175 | // iOS 15 以上的也可以这么设置 176 | if (@available(iOS 15.0, *)) { 177 | UIButtonConfiguration *conf = UIButtonConfiguration.plainButtonConfiguration; 178 | // 这些都可以在 storyboard 和 xib 的属性面板上找到。 179 | conf.imagePadding = space; 180 | // 这玩意可以改变图片的位置,终于可以不用自定义了 181 | conf.imagePlacement = NSDirectionalRectEdgeTrailing; 182 | // 通过 UIButtonConfiguration 创建 UIButton 183 | [UIButton buttonWithConfiguration:conf primaryAction:nil]; 184 | } 185 | 186 | } 187 | 188 | static const int kTimestampRandomIdLength = 16; 189 | // 用时间戳生成一种随机id 190 | + (NSString *)timestampRandomId 191 | { 192 | UInt64 start = (UInt64)(CFAbsoluteTimeGetCurrent() * 1000); 193 | // 从2020 年 1 月 1 日 00:00:00 开始的时间戳。与Android端一致 194 | NSMutableString *userId = [NSMutableString stringWithFormat:@"%llu", start - 599529600000L]; 195 | [userId appendString:arc4random() % 2 == 0 ? @"i" : @"I"]; // 以‘i’或‘I’ 结尾表示iOS端 196 | char cha[] = {'A', '\0'}; 197 | for (NSUInteger i = 0, len = kTimestampRandomIdLength - userId.length; i < len; i++) { 198 | cha[0] = (arc4random() % 2 == 0 ? 'A' : 'a') + arc4random() % 26; 199 | [userId insertString:[NSString stringWithCString:cha encoding:NSASCIIStringEncoding] atIndex:arc4random() % (userId.length - 1)]; 200 | } 201 | 202 | return userId.copy; 203 | } 204 | 205 | @end 206 | 207 | @interface NSObject (SelectedTextRange) @end 208 | @implementation NSObject (SelectedTextRange) 209 | - (void)selectedTextRange 210 | { 211 | // 这里举例是把光标移到 ":" 之前 212 | UITextField *textField = [UITextField new]; 213 | textField.text = @"http://192.168.:8080"; 214 | NSRange range = [textField.text rangeOfString:@":" options:NSBackwardsSearch]; 215 | // NSLog(@"--- %d", range.location == NSNotFound); 216 | if (range.length > 0 && range.location > 5) { 217 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 218 | // 设置光标位置 219 | // 文字起始位置 220 | UITextPosition *beginDocument = textField.beginningOfDocument; 221 | // 自定义 位置 222 | UITextPosition *start = [textField positionFromPosition:beginDocument offset:range.location];//左- 右+ 223 | // 224 | // UITextPosition *end = [textField positionFromPosition:beginDocument offset:range.location];// 开始和结尾不一样时,定义结尾,这样的效果是选中一段文字 225 | textField.selectedTextRange = [textField textRangeFromPosition:start toPosition:start]; 226 | }); 227 | } 228 | } 229 | 230 | @end 231 | -------------------------------------------------------------------------------- /Template/EventKitTemplate.m: -------------------------------------------------------------------------------- 1 | // 2 | // EventKitTemplate.m 3 | // iOS_App_Template 4 | // 5 | // Created by min on 2021/6/16. 6 | // Copyright © 2021 mxm. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | typedef void(^EKEventStoreRequestAccessCompletionHandler)(BOOL granted, NSError * __nullable error); 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | @interface EventKitTemplate : NSObject 18 | 19 | + (BOOL)hasAuthorized; 20 | 21 | + (void)requestAccess:(EKEventStoreRequestAccessCompletionHandler)completion; 22 | 23 | + (NSArray *)queryEvents:(NSString *)eventId; 24 | 25 | + (BOOL)addOrUpdateEventNotifyWithEventId:(NSString *)eventId 26 | title:(NSString *)title 27 | notes:(NSString*)notes 28 | startDate:(NSUInteger)startDate 29 | freq:(NSString *)freq 30 | interval:(NSInteger)interval; 31 | 32 | + (BOOL)removeEvent:(NSString *)eventId; 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | 38 | //iOS10以后获得授权要在plist文件中进行设置:添加权限字符串访问日历:NSCalendarsUsageDescription 访问提醒事项:NSRemindersUsageDescription 39 | //https://www.jianshu.com/p/50751ba43b32 40 | //https://blog.csdn.net/a1661408343/article/details/106356670 41 | @implementation EventKitTemplate 42 | 43 | static EKEventStore *es; 44 | static EKCalendar *myCal; 45 | 46 | + (void)shared 47 | { 48 | static dispatch_once_t onceToken; 49 | dispatch_once(&onceToken, ^{ 50 | es = [EKEventStore new]; 51 | myCal = [self fetchCalendar:es]; 52 | }); 53 | } 54 | 55 | + (BOOL)hasAuthorized 56 | { 57 | return [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent] == EKAuthorizationStatusAuthorized; 58 | //用户授权不允许 59 | // else if (eventStatus == EKAuthorizationStatusDenied){ 60 | // UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"当前日历服务不可用" message:@"您还没有授权本应用使用日历,请到 设置 > 隐私 > 日历 中授权" preferredStyle:UIAlertControllerStyleAlert]; 61 | // UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 62 | // }]; 63 | // [alert addAction:action]; 64 | // [self presentViewController:alert animated:YES completion:nil]; 65 | // } 66 | } 67 | 68 | + (void)requestAccess:(EKEventStoreRequestAccessCompletionHandler)completion 69 | { 70 | //提示用户授权,调出授权弹窗 71 | [[EKEventStore new] requestAccessToEntityType:EKEntityTypeEvent completion:completion]; 72 | // [es requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError * _Nullable error) { 73 | // if (granted) { 74 | // NSLog(@"允许"); 75 | // } else { 76 | // NSLog(@"拒绝授权"); 77 | // } 78 | // }]; 79 | } 80 | 81 | //还有两种授权状态: 82 | //EKAuthorizationStatusAuthorized用户已经允许授权 83 | //EKAuthorizationStatusRestricted,未授权,且用户无法更新,如家长控制情况下 84 | 85 | 86 | #define kEKCalendarTitle @"your calendar account name" 87 | 88 | + (EKCalendar *)fetchCalendar:(EKEventStore *)eventStore 89 | { 90 | for (EKCalendar *ekcalendar in [eventStore calendarsForEntityType:EKEntityTypeEvent]) { 91 | if ([ekcalendar.title isEqualToString:kEKCalendarTitle] ) { 92 | return ekcalendar; 93 | } 94 | } 95 | EKSource *localSource = nil;//EKSource不能创建自己的,因为eventStore没有保存方法 96 | //真机 97 | for (EKSource *source in eventStore.sources){ 98 | if (source.sourceType == EKSourceTypeCalDAV && [source.title isEqualToString:@"iCloud"]){//获取iCloud源 99 | localSource = source; 100 | break; 101 | } 102 | } 103 | 104 | if (localSource == nil) { 105 | //模拟器 106 | for (EKSource *source in eventStore.sources) {//获取本地Local源(就是上面说的模拟器中名为的Default的日历源) 107 | if (source.sourceType == EKSourceTypeLocal) { 108 | localSource = source; 109 | break; 110 | } 111 | } 112 | } 113 | EKCalendar *cal = [EKCalendar calendarForEntityType:EKEntityTypeEvent eventStore:eventStore]; 114 | cal.source = localSource; 115 | cal.title = kEKCalendarTitle;//自定义日历标题 116 | cal.CGColor = [UIColor greenColor].CGColor;//自定义日历颜色 117 | NSError *error; 118 | [eventStore saveCalendar:cal commit:YES error:&error];//EKCalendar可以创建自己的,因为eventStore可以保存 119 | if (error) { 120 | NSLog(@"add EKCalendar error: %@", error); 121 | } 122 | 123 | return cal; 124 | } 125 | 126 | + (NSArray *)queryEvents:(NSString *)eventId 127 | { 128 | [self shared]; 129 | NSArray *events; 130 | if (eventId) { 131 | EKEvent *et = [es eventWithIdentifier:eventId]; 132 | if (et) { 133 | events = @[et]; 134 | } else { 135 | return nil; 136 | } 137 | } else { 138 | NSPredicate *predicate = [es predicateForEventsWithStartDate:[NSDate dateWithTimeIntervalSinceNow:-31536000] endDate:[NSDate dateWithTimeIntervalSinceNow:31536000*2] calendars:@[myCal]];//iOS限制取4年范围内的 139 | events = [es eventsMatchingPredicate:predicate]; 140 | } 141 | 142 | if (nil == events || events.count == 0) { 143 | return nil; 144 | } 145 | 146 | NSLog(@"%@", events); 147 | NSMutableArray *eventJsons = [NSMutableArray arrayWithCapacity:10]; 148 | NSMutableDictionary *eventDic = [NSMutableDictionary dictionaryWithCapacity:10]; 149 | for (EKEvent *event in events) { 150 | eventDic[@"id"] = event.eventIdentifier; 151 | eventDic[@"dtStart"] = @((UInt64)(event.startDate.timeIntervalSince1970 * 1000)); 152 | eventDic[@"title"] = event.title; 153 | eventDic[@"notes"] = event.notes; 154 | EKRecurrenceRule *rrule = event.recurrenceRules.firstObject; 155 | if (rrule) { 156 | eventDic[@"interval"] = @(rrule.interval); 157 | switch (rrule.frequency) { 158 | case EKRecurrenceFrequencyDaily: 159 | eventDic[@"freq"] = @"daily"; 160 | break; 161 | case EKRecurrenceFrequencyWeekly: 162 | eventDic[@"freq"] = @"weekly"; 163 | break; 164 | case EKRecurrenceFrequencyMonthly: 165 | eventDic[@"freq"] = @"monthly"; 166 | break; 167 | case EKRecurrenceFrequencyYearly: 168 | eventDic[@"freq"] = @"yearly"; 169 | break; 170 | default: 171 | break; 172 | } 173 | } 174 | 175 | [eventJsons addObject:eventDic]; 176 | } 177 | return eventJsons; 178 | } 179 | 180 | + (BOOL)addOrUpdateEventNotifyWithEventId:(NSString *)eventId 181 | title:(NSString *)title 182 | notes:(NSString*)notes 183 | startDate:(NSUInteger)startDate 184 | freq:(NSString *)freq 185 | interval:(NSInteger)interval //时间戳,毫秒 186 | { 187 | [self shared]; 188 | // EKReminder *reminder = [EKReminder reminderWithEventStore:es];//没有reminderWithIdentifier方法 189 | //创建一个新事件 190 | EKEvent *event; 191 | if (eventId) { 192 | event = [es eventWithIdentifier:eventId]; 193 | } else { 194 | event = [EKEvent eventWithEventStore:es]; 195 | } 196 | event.title = title;//标题 197 | event.notes = notes;//备注 198 | event.startDate = [NSDate dateWithTimeIntervalSince1970:startDate * 1.0 / 1000];//开始时间 199 | //重复规则 200 | if ([freq caseInsensitiveCompare:@"Daily"] == NSOrderedSame) { 201 | event.recurrenceRules = @[[[EKRecurrenceRule alloc] initRecurrenceWithFrequency:EKRecurrenceFrequencyDaily interval:interval end:nil]]; 202 | } else if ([freq caseInsensitiveCompare:@"Weekly"] == NSOrderedSame) { 203 | event.recurrenceRules = @[[[EKRecurrenceRule alloc] initRecurrenceWithFrequency:EKRecurrenceFrequencyWeekly interval:interval end:nil]]; 204 | 205 | // NSDateComponents *comps = [NSCalendar.currentCalendar components:NSCalendarUnitWeekday fromDate:event.startDate]; 206 | // NSInteger weekdey = [comps weekday];//1、2、3、4、5、6、7 分别对应 周日、周一、周二、周三、周四、周五、周六 207 | // EKRecurrenceRule *rule = [[EKRecurrenceRule alloc] 208 | // initRecurrenceWithFrequency:EKRecurrenceFrequencyWeekly 209 | // interval:interval 210 | // daysOfTheWeek:@[[EKRecurrenceDayOfWeek dayOfWeek:weekdey]] 211 | // daysOfTheMonth:nil 212 | // monthsOfTheYear:nil 213 | // weeksOfTheYear:nil 214 | // daysOfTheYear:nil 215 | // setPositions:nil 216 | // end:nil]; 217 | // event.recurrenceRules = @[rule]; 218 | } else if ([freq caseInsensitiveCompare:@"Monthly"] == NSOrderedSame) { 219 | event.recurrenceRules = @[[[EKRecurrenceRule alloc] initRecurrenceWithFrequency:EKRecurrenceFrequencyMonthly interval:interval end:nil]]; 220 | } else if ([freq caseInsensitiveCompare:@"Yearly"] == NSOrderedSame) { 221 | event.recurrenceRules = @[[[EKRecurrenceRule alloc] initRecurrenceWithFrequency:EKRecurrenceFrequencyYearly interval:interval end:nil]]; 222 | } 223 | 224 | //设置提醒 225 | [event addAlarm:[EKAlarm alarmWithRelativeOffset:0]]; 226 | 227 | [event setCalendar:myCal];//设置日历类型 228 | //保存事件 229 | NSError *err = nil; 230 | if ([es saveEvent:event span:EKSpanFutureEvents commit:YES error:&err]) {//注意这里是no,在外部调用完这个add方法之后一定要commit 231 | NSLog(@"创建事件到系统日历成功!,%@", title); 232 | } else { 233 | NSLog(@"创建失败%@", err); 234 | } 235 | return nil == err; 236 | } 237 | 238 | + (BOOL)removeEvent:(NSString *)eventId 239 | { 240 | [self shared]; 241 | return [es removeEvent:[es eventWithIdentifier:eventId] span:EKSpanFutureEvents error:nil]; 242 | } 243 | 244 | @end 245 | -------------------------------------------------------------------------------- /Template/IPAddr.m: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import 8 | #import 9 | 10 | @interface NetUtils : NSObject 11 | 12 | @end 13 | 14 | @implementation NetUtils 15 | 16 | //"utun0": VPN 17 | 18 | + (NSString *)IPAddressForWiFi 19 | { 20 | struct ifaddrs *interfaces = NULL; 21 | if (getifaddrs(&interfaces) != 0) return nil; 22 | NSString *address = nil; 23 | 24 | for (struct ifaddrs *addr = interfaces; addr != NULL; addr = addr->ifa_next) { 25 | //tvOS:en0:网线网卡;en1:WiFi网卡 26 | //iOS:en0:WiFi网卡 27 | if(addr->ifa_addr->sa_family == AF_INET && 28 | (strcmp("en0", addr->ifa_name) == 0 || strcmp("en1", addr->ifa_name) == 0)) { 29 | address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)addr->ifa_addr)->sin_addr)]; 30 | break; 31 | } 32 | // if (addr->ifa_addr->sa_family == AF_INET6) {//IPv6 33 | // char ip6[INET6_ADDRSTRLEN]; 34 | // if(inet_ntop(AF_INET6, &((struct sockaddr_in6 *)addr->ifa_addr)->sin6_addr, ip6, INET6_ADDRSTRLEN)) { 35 | // address = [NSString stringWithUTF8String:ip6]; 36 | // } 37 | // } 38 | } 39 | // Free memory 40 | freeifaddrs(interfaces); 41 | 42 | return address; 43 | } 44 | 45 | //蜂窝网络地址 46 | + (NSString *)IPAddressForWWAN 47 | { 48 | struct ifaddrs *interfaces = NULL; 49 | if (getifaddrs(&interfaces) != 0) return nil; 50 | NSString *address = nil; 51 | 52 | for (struct ifaddrs *addr = interfaces; addr != NULL; addr = addr->ifa_next) { 53 | if(addr->ifa_addr->sa_family == AF_INET && 54 | strcmp("pdp_ip0", addr->ifa_name) == 0) { 55 | address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)addr->ifa_addr)->sin_addr)]; 56 | break; 57 | } 58 | } 59 | // Free memory 60 | freeifaddrs(interfaces); 61 | 62 | return address; 63 | } 64 | 65 | //项目中还有很多其他的配置要添加,请参照以下链接: 66 | //获取Wi-Fi列表 https://juejin.cn/post/6844903529618866183 67 | //获取当前连接Wi-Fi的名称, https://zhuanlan.zhihu.com/p/76119256 68 | //https://blog.csdn.net/iOS1501101533/article/details/109306856 69 | //必须在“Signing & Capabilities”里添加“Access WiFi Information” 70 | + (NSString *)fetchWiFiName 71 | { 72 | CFArrayRef wifiInterfaces = CNCopySupportedInterfaces(); 73 | if (!wifiInterfaces || CFArrayGetCount(wifiInterfaces) <= 0) { 74 | return nil; 75 | } 76 | NSString *wifiName = nil; 77 | CFIndex count = CFArrayGetCount(wifiInterfaces); 78 | for (CFIndex i = 0; i < count; i++) { 79 | CFDictionaryRef infoDic = CNCopyCurrentNetworkInfo(CFArrayGetValueAtIndex(wifiInterfaces, i)); 80 | if (infoDic) { 81 | wifiName = CFDictionaryGetValue(infoDic, kCNNetworkInfoKeySSID);//BSSID是Mac地址 82 | CFRelease(infoDic); 83 | break; 84 | } 85 | } 86 | CFRelease(wifiInterfaces); 87 | return wifiName; 88 | } 89 | 90 | + (instancetype)shared 91 | { 92 | static dispatch_once_t onceToken; 93 | static NetUtils *instance; 94 | dispatch_once(&onceToken, ^{ 95 | instance = [self new]; 96 | }); 97 | return instance; 98 | } 99 | 100 | typedef void(^WiFiResult)(NSString * _Nullable wifiName); 101 | 102 | static CLLocationManager *lm; 103 | static WiFiResult wifiResult; 104 | //必须在“Signing & Capabilities”里添加“Access WiFi Information” 105 | //此方法必须在主线程中调用,因为CLLocationManager的一些操作会在主线程中 106 | + (void)fetchCurrentWiFiName:(WiFiResult)result 107 | { 108 | if (!CLLocationManager.locationServicesEnabled) { 109 | result(nil); 110 | return; 111 | } 112 | 113 | wifiResult = [result copy]; 114 | dispatch_async(dispatch_get_main_queue(), ^(void) { 115 | lm = [CLLocationManager new]; 116 | lm.delegate = [self shared]; 117 | }); 118 | } 119 | 120 | static void doReslut(BOOL authorized) 121 | { 122 | lm = nil; 123 | if (nil == wifiResult) return; 124 | if (!authorized) { 125 | wifiResult(nil); 126 | wifiResult = nil; 127 | return; 128 | } 129 | if (@available(iOS 14.0, *)) { 130 | //必须在“Signing & Capabilities”里添加“Access WiFi Information” 131 | [NEHotspotNetwork fetchCurrentWithCompletionHandler:^(NEHotspotNetwork * _Nullable currentNetwork) { 132 | wifiResult(currentNetwork.SSID); 133 | wifiResult = nil; 134 | }]; 135 | } else { 136 | wifiResult([NetUtils fetchWiFiName]); 137 | wifiResult = nil; 138 | } 139 | } 140 | 141 | - (BOOL)hasAuthorized:(CLLocationManager *)manager status:(CLAuthorizationStatus)status 142 | { 143 | switch (status) { 144 | case kCLAuthorizationStatusNotDetermined: 145 | [manager requestWhenInUseAuthorization]; 146 | return NO; 147 | case kCLAuthorizationStatusRestricted:// 定位服务授权状态是受限制的。可能是由于活动限制定位服务,用户不能改变。这个状态可能不是用户拒绝的定位服务。 148 | //提醒用户跳到设置界面打开定位权限 149 | doReslut(NO); 150 | // [lm requestWhenInUseAuthorization];//此状态下申请也不会有弹框 151 | return NO; 152 | case kCLAuthorizationStatusDenied://已经被用户明确禁止定位 153 | //提醒用户跳到设置界面打开定位权限 154 | doReslut(NO); 155 | // [lm requestWhenInUseAuthorization];//此状态下申请也不会有弹框 156 | return NO; 157 | 158 | default: 159 | return YES; 160 | } 161 | } 162 | 163 | - (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager API_AVAILABLE(ios(14.0)) 164 | { 165 | if (![self hasAuthorized:manager status:manager.authorizationStatus]) { 166 | return; 167 | } 168 | 169 | static BOOL first = YES; 170 | if (manager.accuracyAuthorization == CLAccuracyAuthorizationFullAccuracy) { 171 | doReslut(YES); 172 | first = YES; 173 | return; 174 | } 175 | if (first) { 176 | //key在 info.plist中配置 177 | [manager requestTemporaryFullAccuracyAuthorizationWithPurposeKey:@"FetchWiFiNameUsageDescription"]; 178 | first = NO; 179 | } else { 180 | doReslut(NO); 181 | } 182 | } 183 | 184 | - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status 185 | { 186 | if (![self hasAuthorized:manager status:status]) { 187 | return; 188 | } 189 | doReslut(YES); 190 | } 191 | 192 | /** 193 | 连接Wi-Fi, ssid: Wi-Fi名称。这个功能,不用申请权限。 194 | 必须在“Signing & Capabilities”里添加“Hotspot Configuration” 195 | SSID中不可以包含空格,否则无法连接成功 196 | SSID填写错误或包含了空格,虽然会创建一个连接,但用户是无法连接网络成功的 197 | 模拟器上无法运行 198 | 手机系统必须为iOS 11及更高系统 199 | 当重复创建连接时,会抛出一个错误,但用户任然能连接上 200 | 若删除应用,所有创建的连接都会消失 201 | 202 | https://qa.1r1g.com/sf/ask/4028706671/ 203 | 苹果的文档指出: 204 | 如果joinOnce设置为true,则仅在配置了热点的应用程序在前台运行时,热点才会保持配置和连接。发生以下任何事件时,都会断开热点并删除其配置: 205 | 206 | 该应用程序在后台停留超过15秒。 207 | 设备进入睡眠状态。 208 | 该应用程序崩溃,退出或被卸载。 209 | 该应用程序将设备连接到其他Wi-Fi网络。 210 | 也许我的应用被错误地识别为前景。 211 | 212 | 设置joinOnce为false可使应用程序保持连接状态,但这不是可接受的解决方案,因为我的设备热点不提供Internet连接,因此不能在应用程序外部使用。 213 | 设置joinOnce = false,但在这种情况下,如果未检测到 Internet连接,可能会提示用户切换到蜂窝网络。用户不会在后台自动切换,但切换到蜂窝网络是首选选项。 214 | */ 215 | + (void)connectWiFi:(NSString *)ssid password:(NSString *)password 216 | { 217 | NEHotspotConfiguration *hc = [[NEHotspotConfiguration alloc] initWithSSID:ssid passphrase:password isWEP:NO]; 218 | hc.joinOnce = YES; 219 | [[NEHotspotConfigurationManager sharedManager] applyConfiguration:hc completionHandler:^(NSError * _Nullable error) { 220 | /* 221 | 不过这里倒是有个奇怪的地方。applyConfiguration方法的回调,密码错误,不对什么的回调没有error。明明是有NEHotspotConfigurationError这个枚举的。 222 | 223 | 所以这个时候你判断是否加入成功就不能通过有没有error来判断了。 224 | 你可以判断ssid名跟你配置的是否一致来判断。 225 | */ 226 | if (error && error.code != NEHotspotConfigurationErrorAlreadyAssociated && error.code != NEHotspotConfigurationErrorUserDenied) { 227 | NSLog(@"加入失败"); 228 | } else if(error.code == NEHotspotConfigurationErrorUserDenied){ 229 | NSLog(@"已取消"); 230 | } else { 231 | NSLog(@"已连接"); 232 | } 233 | }]; 234 | } 235 | 236 | //监听WiFi切换 237 | + (void)onWiFiChange 238 | { 239 | //CFNotificationCenterGetDarwinNotifyCenter 可用于 扩展与app 之间的通信,只可惜不能传递参数 240 | CFNotificationCenterAddObserver( 241 | CFNotificationCenterGetDarwinNotifyCenter(), //center 242 | (__bridge const void *)([NSObject class]), // observer,要有这个,不然下面的无法删除 243 | onWiFiChangeCallback, // callback 244 | CFSTR(kNotifySCNetworkChange), // event name 245 | NULL, // object 246 | CFNotificationSuspensionBehaviorDeliverImmediately 247 | ); 248 | //iOS10起App-Prefs:root=WIFI, iOS10之前prefs:root=WIFI 这个是网上流传的,无效 249 | //iOS11.3 起 App-Prefs:WIFI, 之前用 app-settings:WIFI 这个有效 250 | [UIApplication.sharedApplication openURL:[NSURL URLWithString:@"App-Prefs:WIFI"] options:[NSDictionary dictionary] completionHandler:nil]; 251 | } 252 | 253 | //WiFi切换回调,CFNotificationCenterGetDarwinNotifyCenter下忽略 object 和 userInfo 参数 254 | static void onWiFiChangeCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) 255 | 256 | {//此回调是在主线程中运行 257 | NSString *notifyName = (__bridge NSString *)name; 258 | // if ([notifyName isEqualToString:@"com.apple.system.config.network_change"]) { 259 | if (CFStringCompare(name, CFSTR(kNotifySCNetworkChange), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { 260 | [NSThread sleepForTimeInterval:0.1];//有时候要等一下才能获取到WiFi名称 261 | [NetUtils fetchCurrentWiFiName:^(NSString * _Nullable wifiName) { 262 | NSLog(@"wifi name: %@", wifiName); 263 | }]; 264 | } else { 265 | NSLog(@"intercepted %@", notifyName); 266 | } 267 | //移除监听 268 | CFNotificationCenterRemoveObserver( 269 | CFNotificationCenterGetDarwinNotifyCenter(), 270 | (__bridge const void *)([NSObject class]),//observer 271 | CFSTR(kNotifySCNetworkChange), 272 | NULL 273 | ); 274 | } 275 | 276 | @end 277 | -------------------------------------------------------------------------------- /Template/MDNS/MDNSManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // MDNSManager.m 3 | // AiHome 4 | // 5 | // Created by macmini on 2022/5/10. 6 | // 7 | 8 | #import "MDNSManager.h" 9 | #import 10 | #import "Const.h" 11 | 12 | typedef void(^SwitchCompletionHandler)(int code, NSError * _Nullable error); 13 | 14 | // MDNS有两种方式:NSNetServiceBrowser(已废弃),nw_browser_t 15 | @interface MDNSManager (Delegate) 16 | 17 | @end 18 | 19 | @implementation MDNSManager 20 | { 21 | NSNetServiceBrowser *_netServiceBrowser; 22 | NSMutableArray *_findServices; 23 | NSMutableArray *_removeServices; 24 | NSMutableDictionary *_ipDict; 25 | NSMutableDictionary *_portDict; 26 | SwitchCompletionHandler _completionHandler; 27 | BOOL _isOn; 28 | dispatch_semaphore_t _lock; 29 | } 30 | 31 | + (instancetype)shared 32 | { 33 | static dispatch_once_t onceToken; 34 | static id instance; 35 | dispatch_once(&onceToken, ^{ 36 | instance = [self new]; 37 | }); 38 | return instance; 39 | } 40 | 41 | - (instancetype)init 42 | { 43 | self = [super init]; 44 | if (self) { 45 | _netServiceBrowser = [NSNetServiceBrowser new]; 46 | _netServiceBrowser.delegate = self; 47 | _lock = dispatch_semaphore_create(1); 48 | _findServices = [NSMutableArray arrayWithCapacity:10]; 49 | _removeServices = [NSMutableArray arrayWithCapacity:10]; 50 | _ipDict = [NSMutableDictionary dictionaryWithCapacity:10]; 51 | _portDict = [NSMutableDictionary dictionaryWithCapacity:10]; 52 | } 53 | return self; 54 | } 55 | 56 | - (NSString *)IPForDeviceId:(NSString *)deviceId 57 | { 58 | return _ipDict[deviceId]; 59 | } 60 | 61 | - (int)portForDeviceId:(NSString *)deviceId 62 | { 63 | return [_portDict[deviceId] intValue]; 64 | } 65 | 66 | static nw_browser_t nw_browser; 67 | /* 68 | 库,没有提供类似 NSNetService.hostName 的函数去拿到ip和port; 69 | nw_endpoint_get_bonjour_service_name 拿到的是 NSNetService.name; 70 | Network库,提供了函数,可通过UDP或TCP的方式,直接连接到扫描的到的 nw_endpoint_t; 71 | 具体可查看官方文档或百度 72 | */ 73 | + (void)searchForServices API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0)) 74 | { 75 | static dispatch_once_t onceToken; 76 | dispatch_once(&onceToken, ^{ 77 | nw_browse_descriptor_t nw_bd = nw_browse_descriptor_create_bonjour_service("_iotaithings._udp.", NULL); 78 | nw_browser = nw_browser_create(nw_bd, NULL); 79 | }); 80 | nw_browser_set_state_changed_handler(nw_browser, ^(nw_browser_state_t state, nw_error_t _Nullable error) { 81 | NSLog(@"nw_browser_state %d", state); 82 | switch (state) { 83 | case nw_browser_state_ready: 84 | NSLog(@"nw_browser_state_ready"); 85 | break; 86 | case nw_browser_state_failed: 87 | NSLog(@"nw_browser_state_failed: %@", error); 88 | break; 89 | case nw_browser_state_cancelled: 90 | NSLog(@"nw_browser_state_cancelled"); 91 | break; 92 | case nw_browser_state_waiting: 93 | NSLog(@"nw_browser_state_waiting"); 94 | break; 95 | case nw_browser_state_invalid: 96 | NSLog(@"nw_browser_state_invalid"); 97 | break; 98 | } 99 | }); 100 | 101 | nw_browser_set_browse_results_changed_handler(nw_browser, ^(nw_browse_result_t _Nonnull old_result, nw_browse_result_t _Nonnull new_result, bool batch_complete) { 102 | NSLog(@"batch_complete %@", batch_complete ? @"YES" : @"NO"); 103 | nw_browse_result_change_t browse_result_change = nw_browse_result_get_changes(old_result, new_result); 104 | switch (browse_result_change) { 105 | case nw_browse_result_change_invalid: 106 | case nw_browse_result_change_result_removed: { 107 | nw_endpoint_t endpoint = nw_browse_result_copy_endpoint(old_result); 108 | NSLog(@"移除了:%s", nw_endpoint_get_bonjour_service_name(endpoint)); 109 | } 110 | break; 111 | case nw_browse_result_change_result_added: { 112 | nw_endpoint_t endpoint = nw_browse_result_copy_endpoint(new_result); 113 | NSLog(@"新增了:name: %s", nw_endpoint_get_bonjour_service_name(endpoint)); 114 | nw_endpoint_type_t type = nw_endpoint_get_type(endpoint); 115 | switch (type) { 116 | // 这几个是测试,其实拿不到真正的ip和port 117 | case nw_endpoint_type_address: { 118 | NSLog(@"nw_endpoint_type_t:nw_endpoint_type_address"); 119 | char *ip = nw_endpoint_copy_address_string(endpoint); 120 | NSLog(@"新增了:addr: %s", ip); 121 | free(ip); 122 | NSLog(@"新增了:port: %hu", nw_endpoint_get_port(endpoint)); 123 | } 124 | break; 125 | case nw_endpoint_type_host: { 126 | NSLog(@"nw_endpoint_type_t:nw_endpoint_type_host"); 127 | const char *hostname = nw_endpoint_get_hostname(endpoint); 128 | NSLog(@"新增了:hostname: %s", hostname); 129 | NSLog(@"新增了:port: %hu", nw_endpoint_get_port(endpoint)); 130 | } 131 | break; 132 | case nw_endpoint_type_bonjour_service: 133 | NSLog(@"nw_endpoint_type_t:nw_endpoint_type_bonjour_service"); { 134 | nw_connection_t connection = nw_connection_create(endpoint, nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION)); 135 | nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t _Nullable error) { 136 | NSLog(@"nw_connection_set_state_changed_handler %d -- %@", state, error); 137 | 138 | }); 139 | nw_connection_set_viability_changed_handler(connection, ^(bool value) { 140 | NSLog(@"nw_connection_set_viability_changed_handler %d", value); 141 | }); 142 | 143 | nw_connection_set_queue(connection, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); 144 | nw_connection_start(connection); 145 | // nw_endpoint_handler_set_adaptive_read_handler(); 146 | } 147 | 148 | break; 149 | case nw_endpoint_type_url: 150 | NSLog(@"nw_endpoint_type_t:nw_endpoint_type_url"); 151 | break; 152 | case nw_endpoint_type_invalid: 153 | NSLog(@"nw_endpoint_type_t:nw_endpoint_type_invalid"); 154 | break; 155 | } 156 | } 157 | break; 158 | } 159 | }); 160 | nw_browser_set_queue(nw_browser, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); 161 | nw_browser_start(nw_browser); 162 | } 163 | 164 | + (void)stopSearchForServices API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0)) 165 | { 166 | if (NULL == nw_browser) return; 167 | nw_browser_cancel(nw_browser); 168 | } 169 | 170 | - (void)switchMDNS:(BOOL)onOrOff completionHandler:(void (^)(int code, NSError * _Nullable error))completionHandler; 171 | { 172 | if (_isOn == onOrOff) return; 173 | 174 | dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); 175 | _isOn = onOrOff; 176 | _completionHandler = [completionHandler copy]; 177 | if (_isOn) { 178 | // type 是有固定格式的,_iot 是自定义名,_udp是协议名称,可以百度。 179 | [_netServiceBrowser searchForServicesOfType:@"_iot._udp." inDomain:@""]; 180 | // [MDNSManager searchForServices]; 181 | } else { 182 | [_netServiceBrowser stop]; 183 | } 184 | dispatch_semaphore_signal(_lock); 185 | } 186 | 187 | #pragma mark - NSNetServiceBrowserDelegate 188 | - (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)browser 189 | { 190 | BOOL isOn = _isOn; 191 | _isOn = YES; 192 | SwitchCompletionHandler hander = _completionHandler; 193 | _completionHandler = nil; 194 | if (isOn && hander) { 195 | hander(0, nil); 196 | } 197 | } 198 | 199 | - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didNotSearch:(NSDictionary *)errorDict 200 | { 201 | BOOL isOn = _isOn; 202 | _isOn = NO; 203 | SwitchCompletionHandler hander = _completionHandler; 204 | _completionHandler = nil; 205 | if (isOn && hander) { 206 | hander(10001, nil);//启用mDNS失败 207 | } 208 | } 209 | 210 | - (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser 211 | { 212 | BOOL isOn = _isOn; 213 | _isOn = NO; 214 | SwitchCompletionHandler hander = _completionHandler; 215 | _completionHandler = nil; 216 | if (!isOn && hander) { 217 | hander(0, nil); 218 | } 219 | } 220 | 221 | - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindDomain:(NSString *)domainString moreComing:(BOOL)moreComing 222 | { 223 | 224 | } 225 | 226 | - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindService:(NSNetService *)service moreComing:(BOOL)moreComing 227 | { 228 | dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); 229 | [_findServices addObject:service.name]; 230 | dispatch_semaphore_signal(_lock); 231 | if (moreComing) return; 232 | 233 | // 解析服务,解析后才能拿到 service.hostName ; service.port 234 | // service.delegate = self; 235 | // [service resolveWithTimeout:2]; //解析成功会有回调方法 - (void)netServiceDidResolveAddress:(NSNetService *)sender; 236 | // 这里和嵌入式定的协议是在 name 中包含了 ip和port,所以不用解析了 237 | dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); 238 | NSArray *arr; 239 | for (NSString *name in _findServices) { 240 | arr = [name componentsSeparatedByString:@"_"]; 241 | if (!arr || arr.count < 4) continue; 242 | if (IsEmptyString(arr[1])) continue; 243 | if (IsEmptyString(arr[2])) continue; 244 | if (IsEmptyString(arr[3])) continue; 245 | 246 | _ipDict[arr[1]] = arr[2]; 247 | _portDict[arr[1]] = @([arr[3] intValue]); 248 | } 249 | [_findServices removeAllObjects]; 250 | dispatch_semaphore_signal(_lock); 251 | } 252 | 253 | - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveDomain:(NSString *)domainString moreComing:(BOOL)moreComing 254 | { 255 | 256 | } 257 | 258 | - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing 259 | { 260 | dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); 261 | // 先集中,后删除。 262 | [_removeServices addObject:service.name]; 263 | dispatch_semaphore_signal(_lock); 264 | if (moreComing) return; 265 | 266 | 267 | dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); 268 | NSArray *arr; 269 | for (NSString *name in _removeServices) { 270 | arr = [name componentsSeparatedByString:@"_"]; 271 | if (!arr || arr.count < 4) continue; 272 | 273 | [_ipDict removeObjectForKey:arr[1]]; 274 | [_portDict removeObjectForKey:arr[1]]; 275 | } 276 | [_removeServices removeAllObjects]; 277 | dispatch_semaphore_signal(_lock); 278 | } 279 | 280 | @end 281 | -------------------------------------------------------------------------------- /Utils/InputLimiter/InputLimiter.m: -------------------------------------------------------------------------------- 1 | // 2 | // InputLimiter.m 3 | // 4 | 5 | #import "InputLimiter.h" 6 | #import 7 | 8 | @interface _NumberInputUpperLimiter : InputLimiter 9 | 10 | - (instancetype)initWithTextField:(UITextField *)textField upperLimit:(int)upperLimit; 11 | 12 | @end 13 | 14 | @implementation _NumberInputUpperLimiter 15 | { 16 | int _upperLimit; 17 | NSCharacterSet *_set; 18 | } 19 | 20 | - (instancetype)initWithTextField:(UITextField *)textField upperLimit:(int)upperLimit 21 | { 22 | self = [super init]; 23 | if (self) { 24 | _set = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"]; 25 | _upperLimit = upperLimit; 26 | textField.keyboardType = UIKeyboardTypeNumberPad; 27 | textField.delegate = self; 28 | // [textField addTarget:self action:@selector(valueChanged:) forControlEvents:UIControlEventEditingChanged]; 29 | } 30 | return self; 31 | } 32 | 33 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string 34 | { 35 | // NSLog(string); 36 | if (![_set isSupersetOfSet:[NSCharacterSet characterSetWithCharactersInString:string]]) return NO; 37 | 38 | NSString *currentText = textField.text; 39 | if (nil == currentText) { 40 | currentText = @""; 41 | } 42 | currentText = [currentText stringByReplacingCharactersInRange:range withString:string]; 43 | return currentText.intValue <= _upperLimit; 44 | } 45 | 46 | @end 47 | 48 | @interface _HexadecimalNumberInputUpperLimiter : InputLimiter 49 | 50 | - (instancetype)initWithTextField:(UITextField *)textField upperLimit:(int)upperLimit; 51 | 52 | @end 53 | 54 | @implementation _HexadecimalNumberInputUpperLimiter 55 | { 56 | int _upperLimit; 57 | NSCharacterSet *_set; 58 | } 59 | 60 | - (instancetype)initWithTextField:(UITextField *)textField upperLimit:(int)upperLimit 61 | { 62 | self = [super init]; 63 | if (self) { 64 | _set = [NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEFabcdef"]; 65 | _upperLimit = upperLimit; 66 | textField.keyboardType = UIKeyboardTypeNumbersAndPunctuation; 67 | textField.delegate = self; 68 | // [textField addTarget:self action:@selector(valueChanged:) forControlEvents:UIControlEventEditingChanged]; 69 | } 70 | return self; 71 | } 72 | 73 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string 74 | { 75 | // NSLog(string); 76 | if (![_set isSupersetOfSet:[NSCharacterSet characterSetWithCharactersInString:string]]) return NO; 77 | 78 | NSString *currentText = textField.text; 79 | if (nil == currentText) { 80 | currentText = @""; 81 | } 82 | currentText = [currentText stringByReplacingCharactersInRange:range withString:string]; 83 | const char *ch = [currentText cStringUsingEncoding:NSUTF8StringEncoding]; 84 | int value; 85 | sscanf(ch, "%x", &value); 86 | return value <= _upperLimit; 87 | } 88 | 89 | @end 90 | 91 | @interface _IntegerInputUpperLimiter : InputLimiter 92 | 93 | - (instancetype)initWithTextField:(UITextField *)textField positiveUpperLimit:(int)upperLimit negativeLowerLimit:(int)lowerLimit; 94 | 95 | @end 96 | 97 | @implementation _IntegerInputUpperLimiter 98 | { 99 | int _upperLimit; 100 | int _lowerLimit; 101 | NSCharacterSet *_set; 102 | } 103 | 104 | - (instancetype)initWithTextField:(UITextField *)textField positiveUpperLimit:(int)upperLimit negativeLowerLimit:(int)lowerLimit 105 | { 106 | self = [super init]; 107 | if (self) { 108 | _set = [NSCharacterSet characterSetWithCharactersInString:@"0123456789-"]; 109 | _upperLimit = upperLimit; 110 | textField.keyboardType = UIKeyboardTypeNumbersAndPunctuation; 111 | textField.delegate = self; 112 | } 113 | return self; 114 | } 115 | 116 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string 117 | { 118 | if (![_set isSupersetOfSet:[NSCharacterSet characterSetWithCharactersInString:string]]) return NO; 119 | // 不是正符号或者数组开头 120 | if (range.location != 0 && [string containsString:@"-"]) return NO; 121 | 122 | NSString *text = textField.text; 123 | if (nil == text) { 124 | text = @""; 125 | } 126 | text = [text stringByReplacingCharactersInRange:range withString:string]; 127 | 128 | int value = text.intValue; 129 | return value >= 0 ? value <= _upperLimit : value >= _lowerLimit; 130 | } 131 | 132 | @end 133 | 134 | @interface _FloatInputFractionDigitsUpperLimiter : InputLimiter 135 | 136 | - (instancetype)initWithTextField:(UITextField *)textField fractionDigits:(UInt8)digits upperLimit:(float)upperLimit; 137 | 138 | @end 139 | 140 | @implementation _FloatInputFractionDigitsUpperLimiter 141 | { 142 | float _upperLimit; 143 | UInt8 _fractionDigits; 144 | NSCharacterSet *_set; 145 | } 146 | 147 | - (instancetype)initWithTextField:(UITextField *)textField fractionDigits:(UInt8)digits upperLimit:(float)upperLimit 148 | { 149 | self = [super init]; 150 | if (self) { 151 | _set = [NSCharacterSet characterSetWithCharactersInString:@"0123456789."]; 152 | _upperLimit = upperLimit; 153 | _fractionDigits = digits; 154 | textField.keyboardType = UIKeyboardTypeDecimalPad; 155 | textField.delegate = self; 156 | [textField addTarget:self action:@selector(textFieldEditingChanged:) forControlEvents:UIControlEventEditingChanged]; 157 | } 158 | return self; 159 | } 160 | 161 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string 162 | { 163 | // NSLog(string); 164 | if (![_set isSupersetOfSet:[NSCharacterSet characterSetWithCharactersInString:string]]) return NO; 165 | 166 | NSString *text = textField.text; 167 | if ([text rangeOfString:@"." options:kNilOptions].length != 0 && [@"." isEqualToString:string]) return NO; 168 | 169 | if (nil == text) { 170 | text = string; 171 | } else { 172 | text = [text stringByReplacingCharactersInRange:range withString:string]; 173 | } 174 | return text.floatValue <= _upperLimit; 175 | } 176 | 177 | - (void)textFieldEditingChanged:(UITextField *)textField 178 | { 179 | NSString *text = textField.text; 180 | NSRange ptRange = [text rangeOfString:@"." options:kNilOptions]; 181 | // NSLog(@"location: %lu -- %lu", ptRange.location, text.length - 1 - ptRange.location); 182 | if (ptRange.length != 0 && text.length - 1 - ptRange.location > _fractionDigits) { 183 | textField.text = [text substringToIndex:ptRange.location + 1 + _fractionDigits]; 184 | } 185 | } 186 | 187 | @end 188 | 189 | @interface _RationalNumberInputFractionDigitsUpperLimiter : InputLimiter 190 | 191 | - (instancetype)initWithTextField:(UITextField *)textField fractionDigits:(UInt8)digits positiveUpperLimit:(float)upperLimit negativeLowerLimit:(float)lowerLimit; 192 | 193 | @end 194 | 195 | @implementation _RationalNumberInputFractionDigitsUpperLimiter 196 | { 197 | float _upperLimit; 198 | float _lowerLimit; 199 | UInt8 _fractionDigits; 200 | NSCharacterSet *_set; 201 | } 202 | 203 | - (instancetype)initWithTextField:(UITextField *)textField fractionDigits:(UInt8)digits positiveUpperLimit:(float)upperLimit negativeLowerLimit:(float)lowerLimit 204 | { 205 | self = [super init]; 206 | if (self) { 207 | _set = [NSCharacterSet characterSetWithCharactersInString:@"0123456789.-"]; 208 | _upperLimit = upperLimit; 209 | _lowerLimit = lowerLimit; 210 | _fractionDigits = digits; 211 | textField.keyboardType = UIKeyboardTypeNumbersAndPunctuation; 212 | textField.delegate = self; 213 | [textField addTarget:self action:@selector(textFieldEditingChanged:) forControlEvents:UIControlEventEditingChanged]; 214 | } 215 | return self; 216 | } 217 | 218 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string 219 | { 220 | // NSLog(string); 221 | if (![_set isSupersetOfSet:[NSCharacterSet characterSetWithCharactersInString:string]]) return NO; 222 | 223 | if (range.location != 0 && [string containsString:@"-"]) return NO; 224 | 225 | NSString *text = textField.text; 226 | if ([text rangeOfString:@"." options:kNilOptions].length != 0 && [@"." isEqualToString:string]) return NO; 227 | 228 | if (nil == text) { 229 | text = string; 230 | } else { 231 | text = [text stringByReplacingCharactersInRange:range withString:string]; 232 | } 233 | float value = text.floatValue; 234 | return value >= 0 ? value <= _upperLimit : value >= _lowerLimit; 235 | } 236 | 237 | //- (void)textFieldDidChangeSelection:(UITextField *)textField 238 | //{ 239 | // NSString *text = textField.text; 240 | // NSRange ptRange = [text rangeOfString:@"." options:kNilOptions]; 241 | //// NSLog(@"location: %lu -- %lu", ptRange.location, text.length - 1 - ptRange.location); 242 | // if (ptRange.length != 0 && text.length - 1 - ptRange.location > _fractionDigits) { 243 | // textField.text = [text substringToIndex:ptRange.location + 1 + _fractionDigits]; 244 | // } 245 | //} 246 | 247 | - (void)textFieldEditingChanged:(UITextField *)textField 248 | { 249 | NSString *text = textField.text; 250 | NSRange ptRange = [text rangeOfString:@"." options:kNilOptions]; 251 | // NSLog(@"location: %lu -- %lu", ptRange.location, text.length - 1 - ptRange.location); 252 | if (ptRange.length != 0 && text.length - 1 - ptRange.location > _fractionDigits) { 253 | textField.text = [text substringToIndex:ptRange.location + 1 + _fractionDigits]; 254 | } 255 | } 256 | 257 | @end 258 | 259 | @implementation InputLimiter 260 | 261 | + (nonnull instancetype)limiterNumberTextField:(UITextField *)textField upperLimit:(int)upperLimit 262 | { 263 | return [[_NumberInputUpperLimiter alloc] initWithTextField:textField upperLimit:upperLimit]; 264 | } 265 | 266 | + (instancetype)limiterHexadecimalNumberTextField:(UITextField *)textField upperLimit:(int)upperLimit 267 | { 268 | return [[_HexadecimalNumberInputUpperLimiter alloc] initWithTextField:textField upperLimit:upperLimit]; 269 | } 270 | 271 | + (nonnull instancetype)limiterIntegerTextField:(UITextField *)textField positiveUpperLimit:(int)upperLimit negativeLowerLimit:(int)lowerLimit 272 | { 273 | return [[_IntegerInputUpperLimiter alloc] initWithTextField:textField positiveUpperLimit:upperLimit negativeLowerLimit:lowerLimit]; 274 | } 275 | 276 | + (nonnull instancetype)limiterFloatTextField:(UITextField *)textField fractionDigits:(UInt8)digits upperLimit:(float)upperLimit 277 | { 278 | return [[_FloatInputFractionDigitsUpperLimiter alloc] initWithTextField:textField fractionDigits:digits upperLimit:upperLimit]; 279 | } 280 | 281 | + (nonnull instancetype)limiterRationalNumberTextField:(UITextField *)textField fractionDigits:(UInt8)digits positiveUpperLimit:(float)upperLimit negativeLowerLimit:(float)lowerLimit 282 | { 283 | return [[_RationalNumberInputFractionDigitsUpperLimiter alloc] initWithTextField:textField fractionDigits:digits positiveUpperLimit:upperLimit negativeLowerLimit:lowerLimit]; 284 | } 285 | 286 | - (void)dealloc 287 | { 288 | NSLog(@"%@ dealloc", NSStringFromClass([self class])); 289 | } 290 | 291 | @end 292 | 293 | @implementation UITextField (InputLimiterProperty) 294 | 295 | - (void)setInputLimiter:(InputLimiter *)inputLimiter 296 | { 297 | objc_setAssociatedObject(self, @selector(inputLimiter), inputLimiter, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 298 | } 299 | 300 | - (InputLimiter *)inputLimiter 301 | { 302 | return objc_getAssociatedObject(self, _cmd); 303 | } 304 | 305 | @end 306 | -------------------------------------------------------------------------------- /iOS_App_Template-archive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # 使用方法 4 | # 1.将shell.sh和附件中的Development_ExportOptions.plist,一起放到工程的根目录下 5 | # 2.终端cd到工程的根目录下,执行脚本 格式为 sh 脚本名字.sh 6 | 7 | # 配置信息 8 | 9 | #这里配置完参数则下方不用进行手动输入(用于参数化构建) 10 | ##是否为工作组(.xcworkspace是否存在) 11 | parameter_workspace="1" 12 | ##打包模式,1:release, 2:debug 13 | parameter_configuration="1" 14 | ##打包类型 15 | parameter_type="4" 16 | ##上传类型 17 | parameter_upload="3" 18 | #上传bugly 19 | parameter_bugly="1" 20 | ##上传appstore 21 | ##账号 22 | #parameter_username="" 23 | ##独立密码 24 | #parameter_password="" 25 | 26 | # 打包导出目录 27 | root_path="/Users/mac/Desktop/iOS" 28 | export_options_plist="" 29 | 30 | echo "\033[32m****************\n开始自动打包\n****************\033[0m\n" 31 | 32 | # ==========自动打包配置信息部分========== # 33 | 34 | #进入项目工程目录 35 | cd aihome/platforms/ios 36 | 37 | configRun () { 38 | #获取项目名称 39 | project_name=`find . -name *.xcodeproj | awk -F "[/.]" '{print $(NF-1)}'` 40 | 41 | #获取工程plist配置文件 42 | # 方式一:拼接 43 | # info_plist_path="${project_name}/${project_name}-Info.plist" 44 | # 方式二:从设置中获取 45 | #获取需要的所有设置 46 | build_settings=`xcodebuild -showBuildSettings | grep ' INFOPLIST_FILE \| MARKETING_VERSION \| CURRENT_PROJECT_VERSION '` 47 | #获取 INFOPLIST_FILE 设置 48 | info_plist_path=`echo $build_settings | grep INFOPLIST_FILE` 49 | #截取 '= ' 后面的类容 50 | info_plist_path=${info_plist_path#*= } 51 | 52 | #设置build版本号(可以不进行设置) 53 | # date=`date +"%Y%m%d%H%M"` 54 | # /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $date" "$info_plist_path" 55 | 56 | #app 版本号 57 | bundle_version=`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${info_plist_path}` 58 | if [ $bundle_version = "\$(MARKETING_VERSION)" ]; then 59 | bundle_version=`echo $build_settings | grep MARKETING_VERSION` 60 | bundle_version=${bundle_version#*= } 61 | fi 62 | 63 | echo "\033[33;1mbundle_version=${bundle_version} \033[0m" 64 | 65 | #获取build版本号 66 | bundle_build_version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" ${info_plist_path}` 67 | if [ $bundle_build_version = "\$(CURRENT_PROJECT_VERSION)" ]; then 68 | bundle_build_version=`echo $build_settings | grep CURRENT_PROJECT_VERSION` 69 | bundle_build_version=${bundle_build_version#*= } 70 | fi 71 | echo "\033[33;1mbundle_build_version=${bundle_build_version} \033[0m\n" 72 | 73 | #指定输出ipa路径 74 | export_path_ipa=$root_path 75 | #指定输出归档文件地址 76 | export_path_archive="$root_path/$project_name.xcarchive" 77 | } 78 | 79 | configRun 80 | 81 | echo "\033[32m****************\n自动打包选择配置部分\n****************\033[0m\n" 82 | 83 | # ==========自动打包可选择信息部分========== # 84 | # 输入是否为工作空间 85 | archiveRun () { 86 | echo "\033[36;1m是否是工作空间(输入序号, 按回车即可) \033[0m" 87 | echo "\033[33;1m1. 是 \033[0m" 88 | echo "\033[33;1m2. 否 \033[0m" 89 | 90 | # if [ ${#parameter_workspace} == 0 ]; then 91 | # #读取用户输入 92 | # read parameter_workspace 93 | # sleep 0.5 94 | # fi 95 | 96 | if [ $parameter_workspace = "1" ]; then 97 | echo "\n\033[32m****************\n将采用:xcworkspace\n****************\033[0m\n" 98 | elif [ $parameter_workspace = "2" ]; then 99 | echo "\n\033[32m****************\n将采用:xcodeproj\n****************\033[0m\n" 100 | else 101 | echo "\n\033[31;1m****************\n您输入的参数,无效请重新输入!!! \n****************\033[0m\n" 102 | parameter_workspace="" 103 | archiveRun 104 | fi 105 | } 106 | 107 | archiveRun 108 | 109 | # 输入打包模式 110 | configurationRun () { 111 | echo "\033[36;1m请选择打包模式(输入序号, 按回车即可) \033[0m" 112 | echo "\033[33;1m1. Release \033[0m" 113 | echo "\033[33;1m2. Debug \033[0m" 114 | 115 | # if [ ${#parameter_configuration} == 0 ]; then 116 | # #读取用户输入 117 | # read parameter_configuration 118 | # sleep 0.5 119 | # fi 120 | 121 | if [ $parameter_configuration = "1" ]; then 122 | parameter_configuration="Release" 123 | elif [ $parameter_configuration = "2" ]; then 124 | parameter_configuration="Debug" 125 | else 126 | echo "\n\033[31;1m****************\n您输入的参数,无效请重新输入!!! \n****************\033[0m\n" 127 | parameter_configuration="" 128 | configurationRun 129 | fi 130 | 131 | echo "\n\033[32m****************\n打包模式:${parameter_configuration} \n****************\033[0m\n" 132 | } 133 | 134 | configurationRun 135 | 136 | # 输入打包类型 137 | methodRun () { 138 | echo "\033[36;1m请选择打包方式(输入序号, 按回车即可) \033[0m" 139 | echo "\033[33;1m1. AdHoc(预发) \033[0m" 140 | echo "\033[33;1m2. AppStore(发布) \033[0m" 141 | echo "\033[33;1m3. Enterprise(企业) \033[0m" 142 | echo "\033[33;1m4. Development(测试) \033[0m\n" 143 | 144 | # if [ ${#parameter_type} == 0 ]; then 145 | # #读取用户输入 146 | # read parameter_type 147 | # sleep 0.5 148 | # fi 149 | 150 | if [ $parameter_type = "1" ]; then 151 | parameter_type="AdHoc" 152 | elif [ $parameter_type = "2" ]; then 153 | parameter_type="AppStore" 154 | elif [ $parameter_type = "3" ]; then 155 | parameter_type="Enterprise" 156 | elif [ $parameter_type = "4" ]; then 157 | parameter_type="Development" 158 | else 159 | echo "\n\033[31;1m****************\n您输入的参数,无效请重新输入!!! \n****************\033[0m\n" 160 | parameter_type="" 161 | methodRun 162 | fi 163 | 164 | echo "\033[32m****************\n您选择了 ${parameter_type} 打包类型\n****************\033[0m\n" 165 | } 166 | 167 | methodRun 168 | 169 | # 输入上传类型 170 | publishRun () { 171 | echo "\033[36;1m请选择上传类型(输入序号, 按回车即可) \033[0m" 172 | echo "\033[33;1m1. 蒲公英 \033[0m" 173 | echo "\033[33;1m2. AppStore \033[0m" 174 | echo "\033[33;1m3. 不上传 \033[0m" 175 | 176 | # if [ ${#parameter_upload} == 0 ]; then 177 | # #读取用户输入 178 | # read parameter_upload 179 | # sleep 0.5 180 | # fi 181 | 182 | if [ $parameter_upload = "1" ]; then 183 | echo "\033[32m****************\n您选择了上传 蒲公英\n****************\033[0m\n" 184 | elif [ $parameter_upload = "2" ]; then 185 | echo "\033[32m****************\n您选择了上传 AppStore\n****************\033[0m\n" 186 | elif [ $parameter_upload = "3" ]; then 187 | echo "\033[32m****************\n您选择了不上传\n****************\033[0m\n" 188 | else 189 | echo "\n\033[31;1m**************** 您输入的参数,无效请重新输入!!! ****************\033[0m\n" 190 | parameter_upload="" 191 | publishRun 192 | fi 193 | } 194 | 195 | publishRun 196 | 197 | # 输入是否上传bugly 198 | buglyRun () { 199 | echo "\033[36;1m请选择是否上传bugly(输入序号, 按回车即可) \033[0m" 200 | echo "\033[33;1m1. 不上传 \033[0m" 201 | echo "\033[33;1m2. 上传 \033[0m" 202 | 203 | # if [ ${#parameter_bugly} == 0 ]; then 204 | # #读取用户输入 205 | # read parameter_bugly 206 | # sleep 0.5 207 | # fi 208 | 209 | if [ $parameter_bugly = "1" ]; then 210 | echo "\033[32m****************\n您选择了不上传 bugly\n****************\033[0m\n" 211 | elif [ $parameter_bugly = "2" ]; then 212 | echo "\033[32m****************\n您选择了上传 bugly\n****************\033[0m\n" 213 | else 214 | echo "\n\033[31;1m**************** 您输入的参数,无效请重新输入!!! ****************\033[0m\n" 215 | parameter_bugly="" 216 | buglyRun 217 | fi 218 | } 219 | 220 | buglyRun 221 | 222 | # 输入APPStore信息 223 | appStoreUserNameRun () { 224 | echo "\033[36;1m请输入APPStore账号(输入完毕, 按回车即可) \033[0m" 225 | if [ ${#parameter_username} == 0 ]; then 226 | #读取用户输入 227 | read parameter_username 228 | sleep 0.5 229 | fi 230 | 231 | echo "\033[36;1m请输入APPStore独立密码(输入完毕, 按回车即可) \033[0m" 232 | if [ ${#parameter_password} == 0 ]; then 233 | #读取用户输入 234 | read parameter_password 235 | sleep 0.5 236 | fi 237 | } 238 | 239 | #选择上传 AppStore 240 | if [ $parameter_upload = "2" ]; then 241 | appStoreUserNameRun 242 | fi 243 | 244 | echo "\n\033[32m****************\n打包信息配置完毕,开始进行打包\n****************\033[0m\n" 245 | echo "\n\033[32m****************\n开始清理工程\n****************\033[0m\n" 246 | 247 | #强制删除旧的文件夹 248 | rm -rf $export_path_ipa 249 | 250 | # 指定输出文件目录不存在则创建 251 | if test -d "$export_path_ipa" ; then 252 | echo $export_path_ipa 253 | else 254 | mkdir -pv $export_path_ipa 255 | fi 256 | 257 | # 清理工程 258 | xcodebuild clean -configuration $parameter_configuration -alltargets 259 | 260 | echo "\n\033[32m****************\n清理工程完毕\n****************\033[0m\n" 261 | echo "\n\033[32m****************\n开始编译项目\n****************\033[0m\n" 262 | 263 | # 开始编译 264 | if [ $parameter_workspace = "1" ]; then 265 | #工作空间 266 | xcodebuild archive \ 267 | -workspace ${project_name}.xcworkspace \ 268 | -scheme ${project_name} \ 269 | -configuration ${parameter_configuration} \ 270 | -destination generic/platform=ios \ 271 | -archivePath ${export_path_archive} 272 | else 273 | #不是工作空间 274 | xcodebuild archive \ 275 | -project ${project_name}.xcodeproj \ 276 | -scheme ${project_name} \ 277 | -configuration ${parameter_configuration} \ 278 | -archivePath ${export_path_archive} 279 | fi 280 | 281 | # 检查是否构建成功 282 | # xcarchive 实际是一个文件夹不是一个文件所以使用 -d 判断 283 | if test -d "$export_path_archive" ; then 284 | echo "\n\033[32m****************\n项目编译成功\n****************\033[0m\n" 285 | else 286 | echo "\n\033[32m****************\n项目编译失败\n****************\033[0m\n" 287 | exit 1 288 | fi 289 | 290 | echo "\n\033[32m****************\n开始导出ipa文件\n****************\033[0m\n" 291 | 292 | #ExportOptions.plist 文件的位置,我这里用的是 当前目录下;文件中的teamID需要修改你们开发者账号的 293 | export_options_plist="${parameter_type}_ExportOptions.plist" 294 | 295 | #1、打包命令 296 | #2、归档文件地址 297 | #3、ipa输出地址 298 | #4、ipa打包plist文件地址 299 | xcodebuild -exportArchive \ 300 | -archivePath ${export_path_archive} \ 301 | -configuration ${parameter_configuration} \ 302 | -exportPath ${export_path_ipa} \ 303 | -exportOptionsPlist ${export_options_plist} 304 | 305 | cd $root_path 306 | #app 名字 307 | app_name=`find . -name *.ipa | awk -F "[/.]" '{print $(NF-1)}'` 308 | 309 | #指定输出ipa名称 : project_name + bundle_build_version 310 | ipa_name="$app_name-V$bundle_version($bundle_build_version)" 311 | #ipa最终路径 312 | path_ipa=$export_path_ipa/$ipa_name.ipa 313 | 314 | # 修改ipa文件名称 315 | mv $export_path_ipa/$app_name.ipa $path_ipa 316 | 317 | # 检查文件是否存在 318 | if test -f "$path_ipa" ; then 319 | echo "\n\033[32m****************\n导出 $app_name.ipa 包成功\n****************\033[0m\n" 320 | open $export_path_ipa 321 | else 322 | echo "\n\033[32m****************\n导出 $app_name.ipa 包失败\n****************\033[0m\n" 323 | exit 1 324 | fi 325 | 326 | echo "\n\033[32m****************\n使用Shell脚本打包完毕\n****************\033[0m\n" 327 | 328 | #上传 蒲公英 329 | if [ $parameter_upload = "1" ]; then 330 | echo "\033[32m****************\n开始上传蒲公英\n****************\033[0m\n" 331 | 332 | curl -F "file=@$path_ipa" \ 333 | -F "uKey=e5a9331a3fd25bc36646f831e4d42f2d" \ 334 | -F "_api_key=ce1874dcf4523737c9c1d3eafd99164f" \ 335 | https://upload.pgyer.com/apiv1/app/upload 336 | 337 | echo "\033[32m****************\n上传蒲公英完毕\n****************\033[0m\n" 338 | fi 339 | 340 | #上传 AppStore 341 | if [ $parameter_upload = "2" ]; then 342 | #验证账号密码 343 | if [ ${#parameter_username} != 0 -a ${#parameter_username} != 0 ]; then 344 | echo "\n\033[32m****************\n开始上传AppStore\n****************\033[0m\n" 345 | 346 | #验证APP 347 | xcrun altool --validate-app \ 348 | -f "$path_ipa" \ 349 | -t iOS \ 350 | -u "$parameter_username" \ 351 | -p "$parameter_password" \ 352 | --output-format xml 353 | 354 | #上传APP 355 | xcrun altool --upload-app \ 356 | -f "$path_ipa" \ 357 | -t iOS \ 358 | -u "$parameter_username" \ 359 | -p "$parameter_password" \ 360 | --output-format xml 361 | 362 | echo "\n\033[32m****************\n上传AppStore完毕\n****************\033[0m\n" 363 | fi 364 | fi 365 | 366 | #上传 Bugly 367 | if [ $parameter_bugly = "2" ]; then 368 | echo "\033[32m****************\n开始上传bugly\n****************\033[0m\n" 369 | bugly_app_id="fc42b13a1b" 370 | bugly_app_key="b1fca7f9-29cf-4e64-ab1f-444391c25cfc" 371 | 372 | #dsym 路径 373 | dsymfile_path="${export_path_archive}/dSYMs/${app_name}.app.dSYM" 374 | 375 | zip_path="${export_path_ipa}" 376 | 377 | java -jar buglySymboliOS.jar \ 378 | -i "${dsymfile_path}" \ 379 | -u -id "${bugly_app_id}" \ 380 | -key "${bugly_app_key}" \ 381 | -version "${bundle_version}" \ 382 | -o "${zip_path}" 383 | echo "\033[32m****************\n上传bugly完成\n****************\033[0m\n" 384 | fi 385 | --------------------------------------------------------------------------------