Compare commits
58 Commits
efb04d134e
...
dev_tuozhu
| Author | SHA1 | Date | |
|---|---|---|---|
| abf32e8457 | |||
| efdcf60ed1 | |||
| 7a1b17d060 | |||
| f43f94b94d | |||
| 3e2dc4bcb6 | |||
| 6fb9e56720 | |||
| e2cff76d13 | |||
| 6e57f1c853 | |||
| 4fe18c77dc | |||
| cb5819e330 | |||
| 6f20e6aeb1 | |||
| 5af2612ff7 | |||
| cac2f13b88 | |||
| edf88721da | |||
| 915b329805 | |||
| 1673a2f4be | |||
| e4cebeac85 | |||
| c7021e382e | |||
| ffea9d2022 | |||
| 90c1e7ff6c | |||
| 59d04bb33c | |||
| eb0d3aaa71 | |||
| 10dfe9b1d6 | |||
| 6993bfd682 | |||
| 247a87891e | |||
| 9af91cc4bc | |||
| 4f23118ec0 | |||
| 482756f6f0 | |||
| 85a3694e35 | |||
| f58bf61500 | |||
| 783d088f22 | |||
| 74476cd592 | |||
| 9b43274e93 | |||
| 8ce1d95c8c | |||
| e8c88a6148 | |||
| e218c1bf3d | |||
| c5326a3079 | |||
| 9101ffaab0 | |||
| e594711fa3 | |||
| 7fd084e529 | |||
| 11b25241bf | |||
| 8fcfce7376 | |||
| 23317c9fd4 | |||
| 045d5eaff8 | |||
| 72b6dbb157 | |||
| e78b56e2cb | |||
| 6c05026402 | |||
| 13facba33a | |||
| 935284388c | |||
| 4c7fd9049f | |||
| 0031b7a5f6 | |||
| 02dd204744 | |||
| f28f7de49d | |||
| c2859f888a | |||
| a2b51189aa | |||
| 2f2f20cfc2 | |||
| 377e88b6db | |||
| 1deca2ae5b |
11
CustomKeyboard/CustomKeyboard.entitlements
Normal file
11
CustomKeyboard/CustomKeyboard.entitlements
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.loveKey.nyx.shared</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>kbkeyboardAppExtension</string>
|
||||
</array>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
|
||||
@@ -6,74 +6,274 @@
|
||||
//
|
||||
|
||||
#import "KeyboardViewController.h"
|
||||
#import "KBKeyBoardMainView.h"
|
||||
|
||||
static CGFloat KEYBOARDHEIGHT = 256;
|
||||
#import "KBKey.h"
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBSettingView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBAuthManager.h"
|
||||
#import "KBFullAccessManager.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@interface KeyboardViewController ()
|
||||
@property (nonatomic, strong) UIButton *nextKeyboardButton;
|
||||
static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
|
||||
@interface KeyboardViewController () <KBKeyBoardMainViewDelegate, KBFunctionViewDelegate>
|
||||
@property (nonatomic, strong) UIButton *nextKeyboardButton; // 系统“下一个键盘”按钮(可选)
|
||||
@property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
||||
@property (nonatomic, strong) KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
||||
@property (nonatomic, strong) KBSettingView *settingView; // 设置页
|
||||
@property (nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
||||
@end
|
||||
|
||||
@implementation KeyboardViewController
|
||||
|
||||
- (void)updateViewConstraints {
|
||||
[super updateViewConstraints];
|
||||
|
||||
// Add custom view sizing constraints here
|
||||
{
|
||||
BOOL _kb_didTriggerLoginDeepLinkOnce;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
//
|
||||
// // Perform custom UI setup here
|
||||
// self.nextKeyboardButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
//
|
||||
// [self.nextKeyboardButton setTitle:NSLocalizedString(@"Next Keyboard", @"Title for 'Next Keyboard' button") forState:UIControlStateNormal];
|
||||
// [self.nextKeyboardButton sizeToFit];
|
||||
// self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
//
|
||||
// [self.nextKeyboardButton addTarget:self action:@selector(handleInputModeListFromView:withEvent:) forControlEvents:UIControlEventAllTouchEvents];
|
||||
//
|
||||
// [self.view addSubview:self.nextKeyboardButton];
|
||||
//
|
||||
// [self.nextKeyboardButton.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES;
|
||||
// [self.nextKeyboardButton.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES;
|
||||
|
||||
[self setupUI];
|
||||
// 指定 HUD 的承载视图(扩展里无法取到 App 的 KeyWindow)
|
||||
[KBHUD setContainerView:self.view];
|
||||
// 绑定完全访问管理器,便于统一感知和联动网络开关
|
||||
[[KBFullAccessManager shared] bindInputController:self];
|
||||
__unused id token = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
|
||||
// 如需,可在此刷新与完全访问相关的 UI
|
||||
}];
|
||||
|
||||
// 皮肤变化时,立即应用
|
||||
__unused id token2 = [[NSNotificationCenter defaultCenter] addObserverForName:KBSkinDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
|
||||
[self kb_applyTheme];
|
||||
}];
|
||||
[self kb_applyTheme];
|
||||
}
|
||||
|
||||
|
||||
- (void)setupUI {
|
||||
CGFloat toolBarHeight = 40;
|
||||
CGFloat bottom = 5;
|
||||
CGFloat buttonSpace = 8;
|
||||
CGFloat eachButtonHeight = (KEYBOARDHEIGHT - toolBarHeight - 10 - 8 * 3 - bottom) / 4;
|
||||
|
||||
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 30)];
|
||||
view.backgroundColor = [UIColor redColor];
|
||||
[self.view addSubview:view];
|
||||
}
|
||||
|
||||
|
||||
- (void)viewWillLayoutSubviews
|
||||
{
|
||||
self.nextKeyboardButton.hidden = !self.needsInputModeSwitchKey;
|
||||
[super viewWillLayoutSubviews];
|
||||
}
|
||||
|
||||
- (void)textWillChange:(id<UITextInput>)textInput {
|
||||
// The app is about to change the document's contents. Perform any preparation here.
|
||||
}
|
||||
|
||||
- (void)textDidChange:(id<UITextInput>)textInput {
|
||||
// The app has just changed the document's contents, the document context has been updated.
|
||||
|
||||
UIColor *textColor = nil;
|
||||
if (self.textDocumentProxy.keyboardAppearance == UIKeyboardAppearanceDark) {
|
||||
textColor = [UIColor whiteColor];
|
||||
} else {
|
||||
textColor = [UIColor blackColor];
|
||||
// 固定键盘整体高度;为减少与系统自调整高度产生的告警,将优先级设置为 High
|
||||
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT];
|
||||
h.priority = UILayoutPriorityDefaultHigh; // 750
|
||||
h.active = YES;
|
||||
// 关闭 UIInputView 自适应(某些系统版本会尝试放大为全屏高度导致冲突)
|
||||
if ([self.view isKindOfClass:[UIInputView class]]) {
|
||||
UIInputView *iv = (UIInputView *)self.view;
|
||||
if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
|
||||
iv.allowsSelfSizing = NO;
|
||||
}
|
||||
}
|
||||
[self.nextKeyboardButton setTitleColor:textColor forState:UIControlStateNormal];
|
||||
// 背景图铺底
|
||||
[self.view addSubview:self.bgImageView];
|
||||
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.view);
|
||||
}];
|
||||
// 预置功能面板(默认隐藏),与键盘区域共享相同布局
|
||||
self.functionView.hidden = YES;
|
||||
[self.view addSubview:self.functionView];
|
||||
[self.functionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self.view);
|
||||
make.top.equalTo(self.view).offset(4);
|
||||
make.bottom.equalTo(self.view.mas_bottom).offset(-4);
|
||||
}];
|
||||
|
||||
[self.view addSubview:self.keyBoardMainView];
|
||||
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self.view);
|
||||
make.top.equalTo(self.view).offset(4);
|
||||
make.bottom.equalTo(self.view.mas_bottom).offset(-4);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
/// 切换显示功能面板/键盘主视图
|
||||
- (void)showFunctionPanel:(BOOL)show {
|
||||
// 简单显隐切换,复用相同的布局区域
|
||||
self.functionView.hidden = !show;
|
||||
self.keyBoardMainView.hidden = show;
|
||||
|
||||
// 可选:把当前显示的视图置顶,避免层级遮挡
|
||||
if (show) {
|
||||
[self.view bringSubviewToFront:self.functionView];
|
||||
} else {
|
||||
[self.view bringSubviewToFront:self.keyBoardMainView];
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示/隐藏设置页(高度与 keyBoardMainView 一致),右侧滑入/滑出
|
||||
- (void)showSettingView:(BOOL)show {
|
||||
if (show) {
|
||||
// if (!self.settingView) {
|
||||
self.settingView = [[KBSettingView alloc] init];
|
||||
self.settingView.hidden = YES;
|
||||
[self.view addSubview:self.settingView];
|
||||
[self.settingView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
// 与键盘主视图完全等同的区域,保证高度、宽度一致
|
||||
make.edges.equalTo(self.keyBoardMainView);
|
||||
}];
|
||||
[self.settingView.backButton addTarget:self action:@selector(onTapSettingsBack) forControlEvents:UIControlEventTouchUpInside];
|
||||
// }
|
||||
[self.view bringSubviewToFront:self.settingView];
|
||||
// 以 keyBoardMainView 的实际宽度为准,避免首次添加时 self.view 宽度尚未计算
|
||||
[self.view layoutIfNeeded];
|
||||
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
||||
if (w <= 0) { w = CGRectGetWidth(self.view.bounds); }
|
||||
if (w <= 0) { w = [UIScreen mainScreen].bounds.size.width; }
|
||||
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
||||
self.settingView.hidden = NO;
|
||||
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
self.settingView.transform = CGAffineTransformIdentity;
|
||||
} completion:nil];
|
||||
} else {
|
||||
if (!self.settingView || self.settingView.hidden) return;
|
||||
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
|
||||
if (w <= 0) { w = CGRectGetWidth(self.view.bounds); }
|
||||
if (w <= 0) { w = [UIScreen mainScreen].bounds.size.width; }
|
||||
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
|
||||
} completion:^(BOOL finished) {
|
||||
self.settingView.hidden = YES;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - KBKeyBoardMainViewDelegate
|
||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key {
|
||||
switch (key.type) {
|
||||
case KBKeyTypeCharacter:
|
||||
[self.textDocumentProxy insertText:key.output ?: key.title ?: @""]; break;
|
||||
case KBKeyTypeBackspace:
|
||||
[self.textDocumentProxy deleteBackward]; break;
|
||||
case KBKeyTypeSpace:
|
||||
[self.textDocumentProxy insertText:@" "]; break;
|
||||
case KBKeyTypeReturn:
|
||||
[self.textDocumentProxy insertText:@"\n"]; break;
|
||||
case KBKeyTypeGlobe:
|
||||
[self advanceToNextInputMode]; break;
|
||||
case KBKeyTypeCustom:
|
||||
// 点击自定义键(如“AI”)切换到功能面板
|
||||
[self showFunctionPanel:YES];
|
||||
break;
|
||||
case KBKeyTypeModeChange:
|
||||
case KBKeyTypeShift:
|
||||
// 这些已在 KBKeyBoardMainView/KBKeyboardView 内部处理
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index {
|
||||
if (index == 0) {
|
||||
[self showFunctionPanel:YES];
|
||||
} else {
|
||||
[self showFunctionPanel:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
|
||||
[self showSettingView:YES];
|
||||
}
|
||||
|
||||
// MARK: - KBFunctionViewDelegate
|
||||
- (void)functionView:(KBFunctionView *)functionView didTapToolActionAtIndex:(NSInteger)index {
|
||||
// 需求:当 index == 0 时,切回键盘主视图
|
||||
if (index == 0) {
|
||||
[self showFunctionPanel:NO];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - lazy
|
||||
- (KBKeyBoardMainView *)keyBoardMainView{
|
||||
if (!_keyBoardMainView) {
|
||||
_keyBoardMainView = [[KBKeyBoardMainView alloc] init];
|
||||
_keyBoardMainView.delegate = self;
|
||||
}
|
||||
return _keyBoardMainView;
|
||||
}
|
||||
|
||||
- (KBFunctionView *)functionView{
|
||||
if (!_functionView) {
|
||||
_functionView = [[KBFunctionView alloc] init];
|
||||
_functionView.delegate = self; // 监听功能面板顶部Bar点击
|
||||
}
|
||||
return _functionView;
|
||||
}
|
||||
|
||||
- (KBSettingView *)settingView {
|
||||
if (!_settingView) {
|
||||
_settingView = [[KBSettingView alloc] init];
|
||||
}
|
||||
return _settingView;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onTapSettingsBack {
|
||||
[self showSettingView:NO];
|
||||
}
|
||||
|
||||
|
||||
// 当键盘第一次显示时,尝试唤起主 App 以提示登录(由主 App 决定是否真的弹登录)。
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
if (!_kb_didTriggerLoginDeepLinkOnce) {
|
||||
_kb_didTriggerLoginDeepLinkOnce = YES;
|
||||
// 仅在未登录时尝试拉起主App登录
|
||||
if (!KBAuthManager.shared.isLoggedIn) {
|
||||
[self kb_tryOpenContainerForLoginIfNeeded];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)kb_tryOpenContainerForLoginIfNeeded {
|
||||
// 使用与主 App 一致的自定义 Scheme
|
||||
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
|
||||
if (!url) return;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {
|
||||
// 即使失败也不重复尝试;避免打扰。
|
||||
__unused typeof(weakSelf) selfStrong = weakSelf;
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)kb_applyTheme {
|
||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
|
||||
self.bgImageView.image = img;
|
||||
BOOL hasImg = (img != nil);
|
||||
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||
self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||
// 触发键区按主题重绘
|
||||
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||
// method declared in KBKeyBoardMainView.h
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
if ([self.functionView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[self.functionView performSelector:@selector(kb_applyTheme)];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIImageView *)bgImageView {
|
||||
if (!_bgImageView) {
|
||||
_bgImageView = [[UIImageView alloc] init];
|
||||
_bgImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
_bgImageView.clipsToBounds = YES;
|
||||
}
|
||||
return _bgImageView;
|
||||
}
|
||||
@end
|
||||
|
||||
43
CustomKeyboard/Manager/KBFullAccessManager.h
Normal file
43
CustomKeyboard/Manager/KBFullAccessManager.h
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// KBFullAccessManager.h
|
||||
// 统一封装:检测并管理键盘扩展的“允许完全访问”状态
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBFullAccessState) {
|
||||
KBFullAccessStateUnknown = 0, // 无法确定(降级处理为未开启)
|
||||
KBFullAccessStateDenied, // 未开启完全访问
|
||||
KBFullAccessStateGranted // 已开启完全访问
|
||||
};
|
||||
|
||||
/// 状态变更通知(仅扩展进程内广播)
|
||||
extern NSNotificationName const KBFullAccessChangedNotification;
|
||||
|
||||
/// 键盘扩展“完全访问”状态管理
|
||||
@interface KBFullAccessManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 绑定当前的 UIInputViewController(用于调用系统私有选择器 hasFullAccess;按字符串反射,避免编译期引用)
|
||||
- (void)bindInputController:(UIInputViewController *)ivc;
|
||||
|
||||
/// 当前状态(内部做缓存;如需强制刷新,调用 refresh)
|
||||
- (KBFullAccessState)currentState;
|
||||
|
||||
/// 便捷判断
|
||||
- (BOOL)hasFullAccess;
|
||||
|
||||
/// 立即刷新一次状态(若状态有变化会发送 KBFullAccessChangedNotification)
|
||||
- (void)refresh;
|
||||
|
||||
/// 若未开启,则在传入视图上展示引导弹层(使用现有的 KBFullAccessGuideView);返回是否已开启
|
||||
- (BOOL)ensureFullAccessOrGuideInView:(UIView *)parent;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
100
CustomKeyboard/Manager/KBFullAccessManager.m
Normal file
100
CustomKeyboard/Manager/KBFullAccessManager.m
Normal file
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// KBFullAccessManager.m
|
||||
//
|
||||
// 统一封装“允许完全访问”检测:
|
||||
// 1) 首选:反射调用 UIInputViewController 的 hasFullAccess(避免直接引用私有 API 标识)
|
||||
// 2) 兜底:无法判断时返回 Unknown(上层可按需降级为 Denied 并提示)
|
||||
//
|
||||
|
||||
#import "KBFullAccessManager.h"
|
||||
#import <objc/message.h>
|
||||
#if __has_include("KBNetworkManager.h")
|
||||
#import "KBNetworkManager.h"
|
||||
#endif
|
||||
#if __has_include("KBKeyboardPermissionManager.h")
|
||||
#import "KBKeyboardPermissionManager.h"
|
||||
#endif
|
||||
|
||||
NSNotificationName const KBFullAccessChangedNotification = @"KBFullAccessChangedNotification";
|
||||
|
||||
@interface KBFullAccessManager ()
|
||||
@property (nonatomic, weak) UIInputViewController *ivc;
|
||||
@property (nonatomic, assign) KBFullAccessState state;
|
||||
@end
|
||||
|
||||
@implementation KBFullAccessManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBFullAccessManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBFullAccessManager new]; });
|
||||
return m;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_state = KBFullAccessStateUnknown;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)bindInputController:(UIInputViewController *)ivc {
|
||||
self.ivc = ivc;
|
||||
[self refresh];
|
||||
}
|
||||
|
||||
- (KBFullAccessState)currentState { return _state; }
|
||||
|
||||
- (BOOL)hasFullAccess { return self.state == KBFullAccessStateGranted; }
|
||||
|
||||
- (void)refresh {
|
||||
KBFullAccessState newState = [self p_detectFullAccessState];
|
||||
if (newState != self.state) {
|
||||
self.state = newState;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBFullAccessChangedNotification object:nil];
|
||||
[self p_applySideEffects];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)ensureFullAccessOrGuideInView:(UIView *)parent {
|
||||
[self refresh];
|
||||
if (self.state == KBFullAccessStateGranted) return YES;
|
||||
#if __has_include("KBFullAccessGuideView.h")
|
||||
// 动态引入,避免主 App 编译引用
|
||||
Class guideCls = NSClassFromString(@"KBFullAccessGuideView");
|
||||
if (guideCls && [guideCls respondsToSelector:NSSelectorFromString(@"showInView:")]) {
|
||||
SEL sel = NSSelectorFromString(@"showInView:");
|
||||
((void (*)(id, SEL, UIView *))objc_msgSend)(guideCls, sel, parent);
|
||||
}
|
||||
#endif
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Detect
|
||||
|
||||
// 通过反射调用 hasFullAccess(若系统提供),否则返回 Unknown
|
||||
- (KBFullAccessState)p_detectFullAccessState {
|
||||
UIInputViewController *ivc = self.ivc;
|
||||
if (!ivc) return KBFullAccessStateUnknown;
|
||||
|
||||
SEL sel = NSSelectorFromString(@"hasFullAccess");
|
||||
if ([ivc respondsToSelector:sel]) {
|
||||
BOOL granted = ((BOOL (*)(id, SEL))objc_msgSend)(ivc, sel);
|
||||
return granted ? KBFullAccessStateGranted : KBFullAccessStateDenied;
|
||||
}
|
||||
// 无法判断时标记 Unknown(上层可按需处理为未开启)
|
||||
return KBFullAccessStateUnknown;
|
||||
}
|
||||
|
||||
#pragma mark - Side Effects
|
||||
|
||||
- (void)p_applySideEffects {
|
||||
#if __has_include("KBNetworkManager.h")
|
||||
// 根据完全访问状态切换网络总开关
|
||||
[KBNetworkManager shared].enabled = (self.state == KBFullAccessStateGranted);
|
||||
#endif
|
||||
#if __has_include("KBKeyboardPermissionManager.h")
|
||||
// 上报给主 App:记录最近一次“完全访问”状态(App 将据此决定是否展示引导页)
|
||||
[[KBKeyboardPermissionManager shared] reportFullAccessFromExtension:(self.state == KBFullAccessStateGranted)];
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// MASCompositeConstraint.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 21/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASConstraint.h"
|
||||
#import "MASUtilities.h"
|
||||
|
||||
/**
|
||||
* A group of MASConstraint objects
|
||||
*/
|
||||
@interface MASCompositeConstraint : MASConstraint
|
||||
|
||||
/**
|
||||
* Creates a composite with a predefined array of children
|
||||
*
|
||||
* @param children child MASConstraints
|
||||
*
|
||||
* @return a composite constraint
|
||||
*/
|
||||
- (id)initWithChildren:(NSArray *)children;
|
||||
|
||||
@end
|
||||
@@ -1,183 +0,0 @@
|
||||
//
|
||||
// MASCompositeConstraint.m
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 21/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASCompositeConstraint.h"
|
||||
#import "MASConstraint+Private.h"
|
||||
|
||||
@interface MASCompositeConstraint () <MASConstraintDelegate>
|
||||
|
||||
@property (nonatomic, strong) id mas_key;
|
||||
@property (nonatomic, strong) NSMutableArray *childConstraints;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MASCompositeConstraint
|
||||
|
||||
- (id)initWithChildren:(NSArray *)children {
|
||||
self = [super init];
|
||||
if (!self) return nil;
|
||||
|
||||
_childConstraints = [children mutableCopy];
|
||||
for (MASConstraint *constraint in _childConstraints) {
|
||||
constraint.delegate = self;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - MASConstraintDelegate
|
||||
|
||||
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
|
||||
NSUInteger index = [self.childConstraints indexOfObject:constraint];
|
||||
NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
|
||||
[self.childConstraints replaceObjectAtIndex:index withObject:replacementConstraint];
|
||||
}
|
||||
|
||||
- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
|
||||
id<MASConstraintDelegate> strongDelegate = self.delegate;
|
||||
MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
|
||||
newConstraint.delegate = self;
|
||||
[self.childConstraints addObject:newConstraint];
|
||||
return newConstraint;
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutConstraint multiplier proxies
|
||||
|
||||
- (MASConstraint * (^)(CGFloat))multipliedBy {
|
||||
return ^id(CGFloat multiplier) {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.multipliedBy(multiplier);
|
||||
}
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(CGFloat))dividedBy {
|
||||
return ^id(CGFloat divider) {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.dividedBy(divider);
|
||||
}
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - MASLayoutPriority proxy
|
||||
|
||||
- (MASConstraint * (^)(MASLayoutPriority))priority {
|
||||
return ^id(MASLayoutPriority priority) {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.priority(priority);
|
||||
}
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutRelation proxy
|
||||
|
||||
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
|
||||
return ^id(id attr, NSLayoutRelation relation) {
|
||||
for (MASConstraint *constraint in self.childConstraints.copy) {
|
||||
constraint.equalToWithRelation(attr, relation);
|
||||
}
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - attribute chaining
|
||||
|
||||
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
|
||||
[self constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Animator proxy
|
||||
|
||||
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
|
||||
|
||||
- (MASConstraint *)animator {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
[constraint animator];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark - debug helpers
|
||||
|
||||
- (MASConstraint * (^)(id))key {
|
||||
return ^id(id key) {
|
||||
self.mas_key = key;
|
||||
int i = 0;
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.key([NSString stringWithFormat:@"%@[%d]", key, i++]);
|
||||
}
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutConstraint constant setters
|
||||
|
||||
- (void)setInsets:(MASEdgeInsets)insets {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.insets = insets;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInset:(CGFloat)inset {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.inset = inset;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setOffset:(CGFloat)offset {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSizeOffset:(CGSize)sizeOffset {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.sizeOffset = sizeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setCenterOffset:(CGPoint)centerOffset {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.centerOffset = centerOffset;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - MASConstraint
|
||||
|
||||
- (void)activate {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
[constraint activate];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)deactivate {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
[constraint deactivate];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)install {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
constraint.updateExisting = self.updateExisting;
|
||||
[constraint install];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)uninstall {
|
||||
for (MASConstraint *constraint in self.childConstraints) {
|
||||
[constraint uninstall];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// MASConstraint+Private.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Nick Tymchenko on 29/04/14.
|
||||
// Copyright (c) 2014 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASConstraint.h"
|
||||
|
||||
@protocol MASConstraintDelegate;
|
||||
|
||||
|
||||
@interface MASConstraint ()
|
||||
|
||||
/**
|
||||
* Whether or not to check for an existing constraint instead of adding constraint
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL updateExisting;
|
||||
|
||||
/**
|
||||
* Usually MASConstraintMaker but could be a parent MASConstraint
|
||||
*/
|
||||
@property (nonatomic, weak) id<MASConstraintDelegate> delegate;
|
||||
|
||||
/**
|
||||
* Based on a provided value type, is equal to calling:
|
||||
* NSNumber - setOffset:
|
||||
* NSValue with CGPoint - setPointOffset:
|
||||
* NSValue with CGSize - setSizeOffset:
|
||||
* NSValue with MASEdgeInsets - setInsets:
|
||||
*/
|
||||
- (void)setLayoutConstantWithValue:(NSValue *)value;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface MASConstraint (Abstract)
|
||||
|
||||
/**
|
||||
* Sets the constraint relation to given NSLayoutRelation
|
||||
* returns a block which accepts one of the following:
|
||||
* MASViewAttribute, UIView, NSValue, NSArray
|
||||
* see readme for more details.
|
||||
*/
|
||||
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation;
|
||||
|
||||
/**
|
||||
* Override to set a custom chaining behaviour
|
||||
*/
|
||||
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol MASConstraintDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Notifies the delegate when the constraint needs to be replaced with another constraint. For example
|
||||
* A MASViewConstraint may turn into a MASCompositeConstraint when an array is passed to one of the equality blocks
|
||||
*/
|
||||
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint;
|
||||
|
||||
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute;
|
||||
|
||||
@end
|
||||
@@ -1,272 +0,0 @@
|
||||
//
|
||||
// MASConstraint.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 22/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASUtilities.h"
|
||||
|
||||
/**
|
||||
* Enables Constraints to be created with chainable syntax
|
||||
* Constraint can represent single NSLayoutConstraint (MASViewConstraint)
|
||||
* or a group of NSLayoutConstraints (MASComposisteConstraint)
|
||||
*/
|
||||
@interface MASConstraint : NSObject
|
||||
|
||||
// Chaining Support
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant,
|
||||
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
|
||||
* NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
|
||||
*/
|
||||
- (MASConstraint * (^)(MASEdgeInsets insets))insets;
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant,
|
||||
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
|
||||
* NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
|
||||
*/
|
||||
- (MASConstraint * (^)(CGFloat inset))inset;
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant,
|
||||
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
|
||||
* NSLayoutAttributeWidth, NSLayoutAttributeHeight
|
||||
*/
|
||||
- (MASConstraint * (^)(CGSize offset))sizeOffset;
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant,
|
||||
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
|
||||
* NSLayoutAttributeCenterX, NSLayoutAttributeCenterY
|
||||
*/
|
||||
- (MASConstraint * (^)(CGPoint offset))centerOffset;
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant
|
||||
*/
|
||||
- (MASConstraint * (^)(CGFloat offset))offset;
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant based on a value type
|
||||
*/
|
||||
- (MASConstraint * (^)(NSValue *value))valueOffset;
|
||||
|
||||
/**
|
||||
* Sets the NSLayoutConstraint multiplier property
|
||||
*/
|
||||
- (MASConstraint * (^)(CGFloat multiplier))multipliedBy;
|
||||
|
||||
/**
|
||||
* Sets the NSLayoutConstraint multiplier to 1.0/dividedBy
|
||||
*/
|
||||
- (MASConstraint * (^)(CGFloat divider))dividedBy;
|
||||
|
||||
/**
|
||||
* Sets the NSLayoutConstraint priority to a float or MASLayoutPriority
|
||||
*/
|
||||
- (MASConstraint * (^)(MASLayoutPriority priority))priority;
|
||||
|
||||
/**
|
||||
* Sets the NSLayoutConstraint priority to MASLayoutPriorityLow
|
||||
*/
|
||||
- (MASConstraint * (^)(void))priorityLow;
|
||||
|
||||
/**
|
||||
* Sets the NSLayoutConstraint priority to MASLayoutPriorityMedium
|
||||
*/
|
||||
- (MASConstraint * (^)(void))priorityMedium;
|
||||
|
||||
/**
|
||||
* Sets the NSLayoutConstraint priority to MASLayoutPriorityHigh
|
||||
*/
|
||||
- (MASConstraint * (^)(void))priorityHigh;
|
||||
|
||||
/**
|
||||
* Sets the constraint relation to NSLayoutRelationEqual
|
||||
* returns a block which accepts one of the following:
|
||||
* MASViewAttribute, UIView, NSValue, NSArray
|
||||
* see readme for more details.
|
||||
*/
|
||||
- (MASConstraint * (^)(id attr))equalTo;
|
||||
|
||||
/**
|
||||
* Sets the constraint relation to NSLayoutRelationGreaterThanOrEqual
|
||||
* returns a block which accepts one of the following:
|
||||
* MASViewAttribute, UIView, NSValue, NSArray
|
||||
* see readme for more details.
|
||||
*/
|
||||
- (MASConstraint * (^)(id attr))greaterThanOrEqualTo;
|
||||
|
||||
/**
|
||||
* Sets the constraint relation to NSLayoutRelationLessThanOrEqual
|
||||
* returns a block which accepts one of the following:
|
||||
* MASViewAttribute, UIView, NSValue, NSArray
|
||||
* see readme for more details.
|
||||
*/
|
||||
- (MASConstraint * (^)(id attr))lessThanOrEqualTo;
|
||||
|
||||
/**
|
||||
* Optional semantic property which has no effect but improves the readability of constraint
|
||||
*/
|
||||
- (MASConstraint *)with;
|
||||
|
||||
/**
|
||||
* Optional semantic property which has no effect but improves the readability of constraint
|
||||
*/
|
||||
- (MASConstraint *)and;
|
||||
|
||||
/**
|
||||
* Creates a new MASCompositeConstraint with the called attribute and reciever
|
||||
*/
|
||||
- (MASConstraint *)left;
|
||||
- (MASConstraint *)top;
|
||||
- (MASConstraint *)right;
|
||||
- (MASConstraint *)bottom;
|
||||
- (MASConstraint *)leading;
|
||||
- (MASConstraint *)trailing;
|
||||
- (MASConstraint *)width;
|
||||
- (MASConstraint *)height;
|
||||
- (MASConstraint *)centerX;
|
||||
- (MASConstraint *)centerY;
|
||||
- (MASConstraint *)baseline;
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
- (MASConstraint *)firstBaseline;
|
||||
- (MASConstraint *)lastBaseline;
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
- (MASConstraint *)leftMargin;
|
||||
- (MASConstraint *)rightMargin;
|
||||
- (MASConstraint *)topMargin;
|
||||
- (MASConstraint *)bottomMargin;
|
||||
- (MASConstraint *)leadingMargin;
|
||||
- (MASConstraint *)trailingMargin;
|
||||
- (MASConstraint *)centerXWithinMargins;
|
||||
- (MASConstraint *)centerYWithinMargins;
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
/**
|
||||
* Sets the constraint debug name
|
||||
*/
|
||||
- (MASConstraint * (^)(id key))key;
|
||||
|
||||
// NSLayoutConstraint constant Setters
|
||||
// for use outside of mas_updateConstraints/mas_makeConstraints blocks
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant,
|
||||
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
|
||||
* NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
|
||||
*/
|
||||
- (void)setInsets:(MASEdgeInsets)insets;
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant,
|
||||
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
|
||||
* NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
|
||||
*/
|
||||
- (void)setInset:(CGFloat)inset;
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant,
|
||||
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
|
||||
* NSLayoutAttributeWidth, NSLayoutAttributeHeight
|
||||
*/
|
||||
- (void)setSizeOffset:(CGSize)sizeOffset;
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant,
|
||||
* only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
|
||||
* NSLayoutAttributeCenterX, NSLayoutAttributeCenterY
|
||||
*/
|
||||
- (void)setCenterOffset:(CGPoint)centerOffset;
|
||||
|
||||
/**
|
||||
* Modifies the NSLayoutConstraint constant
|
||||
*/
|
||||
- (void)setOffset:(CGFloat)offset;
|
||||
|
||||
|
||||
// NSLayoutConstraint Installation support
|
||||
|
||||
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
|
||||
/**
|
||||
* Whether or not to go through the animator proxy when modifying the constraint
|
||||
*/
|
||||
@property (nonatomic, copy, readonly) MASConstraint *animator;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Activates an NSLayoutConstraint if it's supported by an OS.
|
||||
* Invokes install otherwise.
|
||||
*/
|
||||
- (void)activate;
|
||||
|
||||
/**
|
||||
* Deactivates previously installed/activated NSLayoutConstraint.
|
||||
*/
|
||||
- (void)deactivate;
|
||||
|
||||
/**
|
||||
* Creates a NSLayoutConstraint and adds it to the appropriate view.
|
||||
*/
|
||||
- (void)install;
|
||||
|
||||
/**
|
||||
* Removes previously installed NSLayoutConstraint
|
||||
*/
|
||||
- (void)uninstall;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/**
|
||||
* Convenience auto-boxing macros for MASConstraint methods.
|
||||
*
|
||||
* Defining MAS_SHORTHAND_GLOBALS will turn on auto-boxing for default syntax.
|
||||
* A potential drawback of this is that the unprefixed macros will appear in global scope.
|
||||
*/
|
||||
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
|
||||
#define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
|
||||
#define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
|
||||
|
||||
#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))
|
||||
|
||||
|
||||
#ifdef MAS_SHORTHAND_GLOBALS
|
||||
|
||||
#define equalTo(...) mas_equalTo(__VA_ARGS__)
|
||||
#define greaterThanOrEqualTo(...) mas_greaterThanOrEqualTo(__VA_ARGS__)
|
||||
#define lessThanOrEqualTo(...) mas_lessThanOrEqualTo(__VA_ARGS__)
|
||||
|
||||
#define offset(...) mas_offset(__VA_ARGS__)
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@interface MASConstraint (AutoboxingSupport)
|
||||
|
||||
/**
|
||||
* Aliases to corresponding relation methods (for shorthand macros)
|
||||
* Also needed to aid autocompletion
|
||||
*/
|
||||
- (MASConstraint * (^)(id attr))mas_equalTo;
|
||||
- (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo;
|
||||
- (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo;
|
||||
|
||||
/**
|
||||
* A dummy method to aid autocompletion
|
||||
*/
|
||||
- (MASConstraint * (^)(id offset))mas_offset;
|
||||
|
||||
@end
|
||||
@@ -1,301 +0,0 @@
|
||||
//
|
||||
// MASConstraint.m
|
||||
// Masonry
|
||||
//
|
||||
// Created by Nick Tymchenko on 1/20/14.
|
||||
//
|
||||
|
||||
#import "MASConstraint.h"
|
||||
#import "MASConstraint+Private.h"
|
||||
|
||||
#define MASMethodNotImplemented() \
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
|
||||
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
|
||||
userInfo:nil]
|
||||
|
||||
@implementation MASConstraint
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (id)init {
|
||||
NSAssert(![self isMemberOfClass:[MASConstraint class]], @"MASConstraint is an abstract class, you should not instantiate it directly.");
|
||||
return [super init];
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutRelation proxies
|
||||
|
||||
- (MASConstraint * (^)(id))equalTo {
|
||||
return ^id(id attribute) {
|
||||
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(id))mas_equalTo {
|
||||
return ^id(id attribute) {
|
||||
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(id))greaterThanOrEqualTo {
|
||||
return ^id(id attribute) {
|
||||
return self.equalToWithRelation(attribute, NSLayoutRelationGreaterThanOrEqual);
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(id))mas_greaterThanOrEqualTo {
|
||||
return ^id(id attribute) {
|
||||
return self.equalToWithRelation(attribute, NSLayoutRelationGreaterThanOrEqual);
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(id))lessThanOrEqualTo {
|
||||
return ^id(id attribute) {
|
||||
return self.equalToWithRelation(attribute, NSLayoutRelationLessThanOrEqual);
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(id))mas_lessThanOrEqualTo {
|
||||
return ^id(id attribute) {
|
||||
return self.equalToWithRelation(attribute, NSLayoutRelationLessThanOrEqual);
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - MASLayoutPriority proxies
|
||||
|
||||
- (MASConstraint * (^)(void))priorityLow {
|
||||
return ^id{
|
||||
self.priority(MASLayoutPriorityDefaultLow);
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(void))priorityMedium {
|
||||
return ^id{
|
||||
self.priority(MASLayoutPriorityDefaultMedium);
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(void))priorityHigh {
|
||||
return ^id{
|
||||
self.priority(MASLayoutPriorityDefaultHigh);
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutConstraint constant proxies
|
||||
|
||||
- (MASConstraint * (^)(MASEdgeInsets))insets {
|
||||
return ^id(MASEdgeInsets insets){
|
||||
self.insets = insets;
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(CGFloat))inset {
|
||||
return ^id(CGFloat inset){
|
||||
self.inset = inset;
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(CGSize))sizeOffset {
|
||||
return ^id(CGSize offset) {
|
||||
self.sizeOffset = offset;
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(CGPoint))centerOffset {
|
||||
return ^id(CGPoint offset) {
|
||||
self.centerOffset = offset;
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(CGFloat))offset {
|
||||
return ^id(CGFloat offset){
|
||||
self.offset = offset;
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(NSValue *value))valueOffset {
|
||||
return ^id(NSValue *offset) {
|
||||
NSAssert([offset isKindOfClass:NSValue.class], @"expected an NSValue offset, got: %@", offset);
|
||||
[self setLayoutConstantWithValue:offset];
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
- (MASConstraint * (^)(id offset))mas_offset {
|
||||
// Will never be called due to macro
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutConstraint constant setter
|
||||
|
||||
- (void)setLayoutConstantWithValue:(NSValue *)value {
|
||||
if ([value isKindOfClass:NSNumber.class]) {
|
||||
self.offset = [(NSNumber *)value doubleValue];
|
||||
} else if (strcmp(value.objCType, @encode(CGPoint)) == 0) {
|
||||
CGPoint point;
|
||||
[value getValue:&point];
|
||||
self.centerOffset = point;
|
||||
} else if (strcmp(value.objCType, @encode(CGSize)) == 0) {
|
||||
CGSize size;
|
||||
[value getValue:&size];
|
||||
self.sizeOffset = size;
|
||||
} else if (strcmp(value.objCType, @encode(MASEdgeInsets)) == 0) {
|
||||
MASEdgeInsets insets;
|
||||
[value getValue:&insets];
|
||||
self.insets = insets;
|
||||
} else {
|
||||
NSAssert(NO, @"attempting to set layout constant with unsupported value: %@", value);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Semantic properties
|
||||
|
||||
- (MASConstraint *)with {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (MASConstraint *)and {
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Chaining
|
||||
|
||||
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {
|
||||
MASMethodNotImplemented();
|
||||
}
|
||||
|
||||
- (MASConstraint *)left {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
|
||||
}
|
||||
|
||||
- (MASConstraint *)top {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
|
||||
}
|
||||
|
||||
- (MASConstraint *)right {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRight];
|
||||
}
|
||||
|
||||
- (MASConstraint *)bottom {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottom];
|
||||
}
|
||||
|
||||
- (MASConstraint *)leading {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading];
|
||||
}
|
||||
|
||||
- (MASConstraint *)trailing {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailing];
|
||||
}
|
||||
|
||||
- (MASConstraint *)width {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
|
||||
}
|
||||
|
||||
- (MASConstraint *)height {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight];
|
||||
}
|
||||
|
||||
- (MASConstraint *)centerX {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterX];
|
||||
}
|
||||
|
||||
- (MASConstraint *)centerY {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterY];
|
||||
}
|
||||
|
||||
- (MASConstraint *)baseline {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBaseline];
|
||||
}
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
- (MASConstraint *)firstBaseline {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeFirstBaseline];
|
||||
}
|
||||
- (MASConstraint *)lastBaseline {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLastBaseline];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
- (MASConstraint *)leftMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeftMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)rightMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRightMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)topMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTopMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)bottomMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottomMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)leadingMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeadingMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)trailingMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailingMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)centerXWithinMargins {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterXWithinMargins];
|
||||
}
|
||||
|
||||
- (MASConstraint *)centerYWithinMargins {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterYWithinMargins];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark - Abstract
|
||||
|
||||
- (MASConstraint * (^)(CGFloat multiplier))multipliedBy { MASMethodNotImplemented(); }
|
||||
|
||||
- (MASConstraint * (^)(CGFloat divider))dividedBy { MASMethodNotImplemented(); }
|
||||
|
||||
- (MASConstraint * (^)(MASLayoutPriority priority))priority { MASMethodNotImplemented(); }
|
||||
|
||||
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); }
|
||||
|
||||
- (MASConstraint * (^)(id key))key { MASMethodNotImplemented(); }
|
||||
|
||||
- (void)setInsets:(MASEdgeInsets __unused)insets { MASMethodNotImplemented(); }
|
||||
|
||||
- (void)setInset:(CGFloat __unused)inset { MASMethodNotImplemented(); }
|
||||
|
||||
- (void)setSizeOffset:(CGSize __unused)sizeOffset { MASMethodNotImplemented(); }
|
||||
|
||||
- (void)setCenterOffset:(CGPoint __unused)centerOffset { MASMethodNotImplemented(); }
|
||||
|
||||
- (void)setOffset:(CGFloat __unused)offset { MASMethodNotImplemented(); }
|
||||
|
||||
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
|
||||
|
||||
- (MASConstraint *)animator { MASMethodNotImplemented(); }
|
||||
|
||||
#endif
|
||||
|
||||
- (void)activate { MASMethodNotImplemented(); }
|
||||
|
||||
- (void)deactivate { MASMethodNotImplemented(); }
|
||||
|
||||
- (void)install { MASMethodNotImplemented(); }
|
||||
|
||||
- (void)uninstall { MASMethodNotImplemented(); }
|
||||
|
||||
@end
|
||||
@@ -1,146 +0,0 @@
|
||||
//
|
||||
// MASConstraintMaker.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 20/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASConstraint.h"
|
||||
#import "MASUtilities.h"
|
||||
|
||||
typedef NS_OPTIONS(NSInteger, MASAttribute) {
|
||||
MASAttributeLeft = 1 << NSLayoutAttributeLeft,
|
||||
MASAttributeRight = 1 << NSLayoutAttributeRight,
|
||||
MASAttributeTop = 1 << NSLayoutAttributeTop,
|
||||
MASAttributeBottom = 1 << NSLayoutAttributeBottom,
|
||||
MASAttributeLeading = 1 << NSLayoutAttributeLeading,
|
||||
MASAttributeTrailing = 1 << NSLayoutAttributeTrailing,
|
||||
MASAttributeWidth = 1 << NSLayoutAttributeWidth,
|
||||
MASAttributeHeight = 1 << NSLayoutAttributeHeight,
|
||||
MASAttributeCenterX = 1 << NSLayoutAttributeCenterX,
|
||||
MASAttributeCenterY = 1 << NSLayoutAttributeCenterY,
|
||||
MASAttributeBaseline = 1 << NSLayoutAttributeBaseline,
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
MASAttributeFirstBaseline = 1 << NSLayoutAttributeFirstBaseline,
|
||||
MASAttributeLastBaseline = 1 << NSLayoutAttributeLastBaseline,
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
MASAttributeLeftMargin = 1 << NSLayoutAttributeLeftMargin,
|
||||
MASAttributeRightMargin = 1 << NSLayoutAttributeRightMargin,
|
||||
MASAttributeTopMargin = 1 << NSLayoutAttributeTopMargin,
|
||||
MASAttributeBottomMargin = 1 << NSLayoutAttributeBottomMargin,
|
||||
MASAttributeLeadingMargin = 1 << NSLayoutAttributeLeadingMargin,
|
||||
MASAttributeTrailingMargin = 1 << NSLayoutAttributeTrailingMargin,
|
||||
MASAttributeCenterXWithinMargins = 1 << NSLayoutAttributeCenterXWithinMargins,
|
||||
MASAttributeCenterYWithinMargins = 1 << NSLayoutAttributeCenterYWithinMargins,
|
||||
|
||||
#endif
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides factory methods for creating MASConstraints.
|
||||
* Constraints are collected until they are ready to be installed
|
||||
*
|
||||
*/
|
||||
@interface MASConstraintMaker : NSObject
|
||||
|
||||
/**
|
||||
* The following properties return a new MASViewConstraint
|
||||
* with the first item set to the makers associated view and the appropriate MASViewAttribute
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) MASConstraint *left;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *top;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *right;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *bottom;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *leading;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *trailing;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *width;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *height;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *centerX;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *centerY;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *baseline;
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
@property (nonatomic, strong, readonly) MASConstraint *firstBaseline;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *lastBaseline;
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
@property (nonatomic, strong, readonly) MASConstraint *leftMargin;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *rightMargin;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *topMargin;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *bottomMargin;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *leadingMargin;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *trailingMargin;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *centerXWithinMargins;
|
||||
@property (nonatomic, strong, readonly) MASConstraint *centerYWithinMargins;
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Returns a block which creates a new MASCompositeConstraint with the first item set
|
||||
* to the makers associated view and children corresponding to the set bits in the
|
||||
* MASAttribute parameter. Combine multiple attributes via binary-or.
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) MASConstraint *(^attributes)(MASAttribute attrs);
|
||||
|
||||
/**
|
||||
* Creates a MASCompositeConstraint with type MASCompositeConstraintTypeEdges
|
||||
* which generates the appropriate MASViewConstraint children (top, left, bottom, right)
|
||||
* with the first item set to the makers associated view
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) MASConstraint *edges;
|
||||
|
||||
/**
|
||||
* Creates a MASCompositeConstraint with type MASCompositeConstraintTypeSize
|
||||
* which generates the appropriate MASViewConstraint children (width, height)
|
||||
* with the first item set to the makers associated view
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) MASConstraint *size;
|
||||
|
||||
/**
|
||||
* Creates a MASCompositeConstraint with type MASCompositeConstraintTypeCenter
|
||||
* which generates the appropriate MASViewConstraint children (centerX, centerY)
|
||||
* with the first item set to the makers associated view
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) MASConstraint *center;
|
||||
|
||||
/**
|
||||
* Whether or not to check for an existing constraint instead of adding constraint
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL updateExisting;
|
||||
|
||||
/**
|
||||
* Whether or not to remove existing constraints prior to installing
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL removeExisting;
|
||||
|
||||
/**
|
||||
* initialises the maker with a default view
|
||||
*
|
||||
* @param view any MASConstraint are created with this view as the first item
|
||||
*
|
||||
* @return a new MASConstraintMaker
|
||||
*/
|
||||
- (id)initWithView:(MAS_VIEW *)view;
|
||||
|
||||
/**
|
||||
* Calls install method on any MASConstraints which have been created by this maker
|
||||
*
|
||||
* @return an array of all the installed MASConstraints
|
||||
*/
|
||||
- (NSArray *)install;
|
||||
|
||||
- (MASConstraint * (^)(dispatch_block_t))group;
|
||||
|
||||
@end
|
||||
@@ -1,273 +0,0 @@
|
||||
//
|
||||
// MASConstraintMaker.m
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 20/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASConstraintMaker.h"
|
||||
#import "MASViewConstraint.h"
|
||||
#import "MASCompositeConstraint.h"
|
||||
#import "MASConstraint+Private.h"
|
||||
#import "MASViewAttribute.h"
|
||||
#import "View+MASAdditions.h"
|
||||
|
||||
@interface MASConstraintMaker () <MASConstraintDelegate>
|
||||
|
||||
@property (nonatomic, weak) MAS_VIEW *view;
|
||||
@property (nonatomic, strong) NSMutableArray *constraints;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MASConstraintMaker
|
||||
|
||||
- (id)initWithView:(MAS_VIEW *)view {
|
||||
self = [super init];
|
||||
if (!self) return nil;
|
||||
|
||||
self.view = view;
|
||||
self.constraints = NSMutableArray.new;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray *)install {
|
||||
if (self.removeExisting) {
|
||||
NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
|
||||
for (MASConstraint *constraint in installedConstraints) {
|
||||
[constraint uninstall];
|
||||
}
|
||||
}
|
||||
NSArray *constraints = self.constraints.copy;
|
||||
for (MASConstraint *constraint in constraints) {
|
||||
constraint.updateExisting = self.updateExisting;
|
||||
[constraint install];
|
||||
}
|
||||
[self.constraints removeAllObjects];
|
||||
return constraints;
|
||||
}
|
||||
|
||||
#pragma mark - MASConstraintDelegate
|
||||
|
||||
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
|
||||
NSUInteger index = [self.constraints indexOfObject:constraint];
|
||||
NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
|
||||
[self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
|
||||
}
|
||||
|
||||
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
|
||||
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
|
||||
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
|
||||
if ([constraint isKindOfClass:MASViewConstraint.class]) {
|
||||
//replace with composite constraint
|
||||
NSArray *children = @[constraint, newConstraint];
|
||||
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
|
||||
compositeConstraint.delegate = self;
|
||||
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
|
||||
return compositeConstraint;
|
||||
}
|
||||
if (!constraint) {
|
||||
newConstraint.delegate = self;
|
||||
[self.constraints addObject:newConstraint];
|
||||
}
|
||||
return newConstraint;
|
||||
}
|
||||
|
||||
- (MASConstraint *)addConstraintWithAttributes:(MASAttribute)attrs {
|
||||
__unused MASAttribute anyAttribute = (MASAttributeLeft | MASAttributeRight | MASAttributeTop | MASAttributeBottom | MASAttributeLeading
|
||||
| MASAttributeTrailing | MASAttributeWidth | MASAttributeHeight | MASAttributeCenterX
|
||||
| MASAttributeCenterY | MASAttributeBaseline
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
| MASAttributeFirstBaseline | MASAttributeLastBaseline
|
||||
#endif
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
| MASAttributeLeftMargin | MASAttributeRightMargin | MASAttributeTopMargin | MASAttributeBottomMargin
|
||||
| MASAttributeLeadingMargin | MASAttributeTrailingMargin | MASAttributeCenterXWithinMargins
|
||||
| MASAttributeCenterYWithinMargins
|
||||
#endif
|
||||
);
|
||||
|
||||
NSAssert((attrs & anyAttribute) != 0, @"You didn't pass any attribute to make.attributes(...)");
|
||||
|
||||
NSMutableArray *attributes = [NSMutableArray array];
|
||||
|
||||
if (attrs & MASAttributeLeft) [attributes addObject:self.view.mas_left];
|
||||
if (attrs & MASAttributeRight) [attributes addObject:self.view.mas_right];
|
||||
if (attrs & MASAttributeTop) [attributes addObject:self.view.mas_top];
|
||||
if (attrs & MASAttributeBottom) [attributes addObject:self.view.mas_bottom];
|
||||
if (attrs & MASAttributeLeading) [attributes addObject:self.view.mas_leading];
|
||||
if (attrs & MASAttributeTrailing) [attributes addObject:self.view.mas_trailing];
|
||||
if (attrs & MASAttributeWidth) [attributes addObject:self.view.mas_width];
|
||||
if (attrs & MASAttributeHeight) [attributes addObject:self.view.mas_height];
|
||||
if (attrs & MASAttributeCenterX) [attributes addObject:self.view.mas_centerX];
|
||||
if (attrs & MASAttributeCenterY) [attributes addObject:self.view.mas_centerY];
|
||||
if (attrs & MASAttributeBaseline) [attributes addObject:self.view.mas_baseline];
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
if (attrs & MASAttributeFirstBaseline) [attributes addObject:self.view.mas_firstBaseline];
|
||||
if (attrs & MASAttributeLastBaseline) [attributes addObject:self.view.mas_lastBaseline];
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
if (attrs & MASAttributeLeftMargin) [attributes addObject:self.view.mas_leftMargin];
|
||||
if (attrs & MASAttributeRightMargin) [attributes addObject:self.view.mas_rightMargin];
|
||||
if (attrs & MASAttributeTopMargin) [attributes addObject:self.view.mas_topMargin];
|
||||
if (attrs & MASAttributeBottomMargin) [attributes addObject:self.view.mas_bottomMargin];
|
||||
if (attrs & MASAttributeLeadingMargin) [attributes addObject:self.view.mas_leadingMargin];
|
||||
if (attrs & MASAttributeTrailingMargin) [attributes addObject:self.view.mas_trailingMargin];
|
||||
if (attrs & MASAttributeCenterXWithinMargins) [attributes addObject:self.view.mas_centerXWithinMargins];
|
||||
if (attrs & MASAttributeCenterYWithinMargins) [attributes addObject:self.view.mas_centerYWithinMargins];
|
||||
|
||||
#endif
|
||||
|
||||
NSMutableArray *children = [NSMutableArray arrayWithCapacity:attributes.count];
|
||||
|
||||
for (MASViewAttribute *a in attributes) {
|
||||
[children addObject:[[MASViewConstraint alloc] initWithFirstViewAttribute:a]];
|
||||
}
|
||||
|
||||
MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children];
|
||||
constraint.delegate = self;
|
||||
[self.constraints addObject:constraint];
|
||||
return constraint;
|
||||
}
|
||||
|
||||
#pragma mark - standard Attributes
|
||||
|
||||
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
|
||||
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
|
||||
}
|
||||
|
||||
- (MASConstraint *)left {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
|
||||
}
|
||||
|
||||
- (MASConstraint *)top {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
|
||||
}
|
||||
|
||||
- (MASConstraint *)right {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRight];
|
||||
}
|
||||
|
||||
- (MASConstraint *)bottom {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottom];
|
||||
}
|
||||
|
||||
- (MASConstraint *)leading {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading];
|
||||
}
|
||||
|
||||
- (MASConstraint *)trailing {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailing];
|
||||
}
|
||||
|
||||
- (MASConstraint *)width {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
|
||||
}
|
||||
|
||||
- (MASConstraint *)height {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight];
|
||||
}
|
||||
|
||||
- (MASConstraint *)centerX {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterX];
|
||||
}
|
||||
|
||||
- (MASConstraint *)centerY {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterY];
|
||||
}
|
||||
|
||||
- (MASConstraint *)baseline {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBaseline];
|
||||
}
|
||||
|
||||
- (MASConstraint *(^)(MASAttribute))attributes {
|
||||
return ^(MASAttribute attrs){
|
||||
return [self addConstraintWithAttributes:attrs];
|
||||
};
|
||||
}
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
- (MASConstraint *)firstBaseline {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeFirstBaseline];
|
||||
}
|
||||
|
||||
- (MASConstraint *)lastBaseline {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLastBaseline];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
- (MASConstraint *)leftMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeftMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)rightMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRightMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)topMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTopMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)bottomMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottomMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)leadingMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeadingMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)trailingMargin {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailingMargin];
|
||||
}
|
||||
|
||||
- (MASConstraint *)centerXWithinMargins {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterXWithinMargins];
|
||||
}
|
||||
|
||||
- (MASConstraint *)centerYWithinMargins {
|
||||
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterYWithinMargins];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#pragma mark - composite Attributes
|
||||
|
||||
- (MASConstraint *)edges {
|
||||
return [self addConstraintWithAttributes:MASAttributeTop | MASAttributeLeft | MASAttributeRight | MASAttributeBottom];
|
||||
}
|
||||
|
||||
- (MASConstraint *)size {
|
||||
return [self addConstraintWithAttributes:MASAttributeWidth | MASAttributeHeight];
|
||||
}
|
||||
|
||||
- (MASConstraint *)center {
|
||||
return [self addConstraintWithAttributes:MASAttributeCenterX | MASAttributeCenterY];
|
||||
}
|
||||
|
||||
#pragma mark - grouping
|
||||
|
||||
- (MASConstraint *(^)(dispatch_block_t group))group {
|
||||
return ^id(dispatch_block_t group) {
|
||||
NSInteger previousCount = self.constraints.count;
|
||||
group();
|
||||
|
||||
NSArray *children = [self.constraints subarrayWithRange:NSMakeRange(previousCount, self.constraints.count - previousCount)];
|
||||
MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children];
|
||||
constraint.delegate = self;
|
||||
return constraint;
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// MASLayoutConstraint.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 3/08/13.
|
||||
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASUtilities.h"
|
||||
|
||||
/**
|
||||
* When you are debugging or printing the constraints attached to a view this subclass
|
||||
* makes it easier to identify which constraints have been created via Masonry
|
||||
*/
|
||||
@interface MASLayoutConstraint : NSLayoutConstraint
|
||||
|
||||
/**
|
||||
* a key to associate with this constraint
|
||||
*/
|
||||
@property (nonatomic, strong) id mas_key;
|
||||
|
||||
@end
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// MASLayoutConstraint.m
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 3/08/13.
|
||||
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASLayoutConstraint.h"
|
||||
|
||||
@implementation MASLayoutConstraint
|
||||
|
||||
@end
|
||||
@@ -1,136 +0,0 @@
|
||||
//
|
||||
// MASUtilities.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 19/08/13.
|
||||
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
|
||||
#if TARGET_OS_IPHONE || TARGET_OS_TV
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#define MAS_VIEW UIView
|
||||
#define MAS_VIEW_CONTROLLER UIViewController
|
||||
#define MASEdgeInsets UIEdgeInsets
|
||||
|
||||
typedef UILayoutPriority MASLayoutPriority;
|
||||
static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired;
|
||||
static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh;
|
||||
static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 500;
|
||||
static const MASLayoutPriority MASLayoutPriorityDefaultLow = UILayoutPriorityDefaultLow;
|
||||
static const MASLayoutPriority MASLayoutPriorityFittingSizeLevel = UILayoutPriorityFittingSizeLevel;
|
||||
|
||||
#elif TARGET_OS_MAC
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#define MAS_VIEW NSView
|
||||
#define MASEdgeInsets NSEdgeInsets
|
||||
|
||||
typedef NSLayoutPriority MASLayoutPriority;
|
||||
static const MASLayoutPriority MASLayoutPriorityRequired = NSLayoutPriorityRequired;
|
||||
static const MASLayoutPriority MASLayoutPriorityDefaultHigh = NSLayoutPriorityDefaultHigh;
|
||||
static const MASLayoutPriority MASLayoutPriorityDragThatCanResizeWindow = NSLayoutPriorityDragThatCanResizeWindow;
|
||||
static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 501;
|
||||
static const MASLayoutPriority MASLayoutPriorityWindowSizeStayPut = NSLayoutPriorityWindowSizeStayPut;
|
||||
static const MASLayoutPriority MASLayoutPriorityDragThatCannotResizeWindow = NSLayoutPriorityDragThatCannotResizeWindow;
|
||||
static const MASLayoutPriority MASLayoutPriorityDefaultLow = NSLayoutPriorityDefaultLow;
|
||||
static const MASLayoutPriority MASLayoutPriorityFittingSizeCompression = NSLayoutPriorityFittingSizeCompression;
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Allows you to attach keys to objects matching the variable names passed.
|
||||
*
|
||||
* view1.mas_key = @"view1", view2.mas_key = @"view2";
|
||||
*
|
||||
* is equivalent to:
|
||||
*
|
||||
* MASAttachKeys(view1, view2);
|
||||
*/
|
||||
#define MASAttachKeys(...) \
|
||||
{ \
|
||||
NSDictionary *keyPairs = NSDictionaryOfVariableBindings(__VA_ARGS__); \
|
||||
for (id key in keyPairs.allKeys) { \
|
||||
id obj = keyPairs[key]; \
|
||||
NSAssert([obj respondsToSelector:@selector(setMas_key:)], \
|
||||
@"Cannot attach mas_key to %@", obj); \
|
||||
[obj setMas_key:key]; \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to create object hashes
|
||||
* Based on http://www.mikeash.com/pyblog/friday-qa-2010-06-18-implementing-equality-and-hashing.html
|
||||
*/
|
||||
#define MAS_NSUINT_BIT (CHAR_BIT * sizeof(NSUInteger))
|
||||
#define MAS_NSUINTROTATE(val, howmuch) ((((NSUInteger)val) << howmuch) | (((NSUInteger)val) >> (MAS_NSUINT_BIT - howmuch)))
|
||||
|
||||
/**
|
||||
* Given a scalar or struct value, wraps it in NSValue
|
||||
* Based on EXPObjectify: https://github.com/specta/expecta
|
||||
*/
|
||||
static inline id _MASBoxValue(const char *type, ...) {
|
||||
va_list v;
|
||||
va_start(v, type);
|
||||
id obj = nil;
|
||||
if (strcmp(type, @encode(id)) == 0) {
|
||||
id actual = va_arg(v, id);
|
||||
obj = actual;
|
||||
} else if (strcmp(type, @encode(CGPoint)) == 0) {
|
||||
CGPoint actual = (CGPoint)va_arg(v, CGPoint);
|
||||
obj = [NSValue value:&actual withObjCType:type];
|
||||
} else if (strcmp(type, @encode(CGSize)) == 0) {
|
||||
CGSize actual = (CGSize)va_arg(v, CGSize);
|
||||
obj = [NSValue value:&actual withObjCType:type];
|
||||
} else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
|
||||
MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
|
||||
obj = [NSValue value:&actual withObjCType:type];
|
||||
} else if (strcmp(type, @encode(double)) == 0) {
|
||||
double actual = (double)va_arg(v, double);
|
||||
obj = [NSNumber numberWithDouble:actual];
|
||||
} else if (strcmp(type, @encode(float)) == 0) {
|
||||
float actual = (float)va_arg(v, double);
|
||||
obj = [NSNumber numberWithFloat:actual];
|
||||
} else if (strcmp(type, @encode(int)) == 0) {
|
||||
int actual = (int)va_arg(v, int);
|
||||
obj = [NSNumber numberWithInt:actual];
|
||||
} else if (strcmp(type, @encode(long)) == 0) {
|
||||
long actual = (long)va_arg(v, long);
|
||||
obj = [NSNumber numberWithLong:actual];
|
||||
} else if (strcmp(type, @encode(long long)) == 0) {
|
||||
long long actual = (long long)va_arg(v, long long);
|
||||
obj = [NSNumber numberWithLongLong:actual];
|
||||
} else if (strcmp(type, @encode(short)) == 0) {
|
||||
short actual = (short)va_arg(v, int);
|
||||
obj = [NSNumber numberWithShort:actual];
|
||||
} else if (strcmp(type, @encode(char)) == 0) {
|
||||
char actual = (char)va_arg(v, int);
|
||||
obj = [NSNumber numberWithChar:actual];
|
||||
} else if (strcmp(type, @encode(bool)) == 0) {
|
||||
bool actual = (bool)va_arg(v, int);
|
||||
obj = [NSNumber numberWithBool:actual];
|
||||
} else if (strcmp(type, @encode(unsigned char)) == 0) {
|
||||
unsigned char actual = (unsigned char)va_arg(v, unsigned int);
|
||||
obj = [NSNumber numberWithUnsignedChar:actual];
|
||||
} else if (strcmp(type, @encode(unsigned int)) == 0) {
|
||||
unsigned int actual = (unsigned int)va_arg(v, unsigned int);
|
||||
obj = [NSNumber numberWithUnsignedInt:actual];
|
||||
} else if (strcmp(type, @encode(unsigned long)) == 0) {
|
||||
unsigned long actual = (unsigned long)va_arg(v, unsigned long);
|
||||
obj = [NSNumber numberWithUnsignedLong:actual];
|
||||
} else if (strcmp(type, @encode(unsigned long long)) == 0) {
|
||||
unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
|
||||
obj = [NSNumber numberWithUnsignedLongLong:actual];
|
||||
} else if (strcmp(type, @encode(unsigned short)) == 0) {
|
||||
unsigned short actual = (unsigned short)va_arg(v, unsigned int);
|
||||
obj = [NSNumber numberWithUnsignedShort:actual];
|
||||
}
|
||||
va_end(v);
|
||||
return obj;
|
||||
}
|
||||
|
||||
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))
|
||||
@@ -1,49 +0,0 @@
|
||||
//
|
||||
// MASViewAttribute.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 21/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASUtilities.h"
|
||||
|
||||
/**
|
||||
* An immutable tuple which stores the view and the related NSLayoutAttribute.
|
||||
* Describes part of either the left or right hand side of a constraint equation
|
||||
*/
|
||||
@interface MASViewAttribute : NSObject
|
||||
|
||||
/**
|
||||
* The view which the reciever relates to. Can be nil if item is not a view.
|
||||
*/
|
||||
@property (nonatomic, weak, readonly) MAS_VIEW *view;
|
||||
|
||||
/**
|
||||
* The item which the reciever relates to.
|
||||
*/
|
||||
@property (nonatomic, weak, readonly) id item;
|
||||
|
||||
/**
|
||||
* The attribute which the reciever relates to
|
||||
*/
|
||||
@property (nonatomic, assign, readonly) NSLayoutAttribute layoutAttribute;
|
||||
|
||||
/**
|
||||
* Convenience initializer.
|
||||
*/
|
||||
- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute;
|
||||
|
||||
/**
|
||||
* The designated initializer.
|
||||
*/
|
||||
- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute;
|
||||
|
||||
/**
|
||||
* Determine whether the layoutAttribute is a size attribute
|
||||
*
|
||||
* @return YES if layoutAttribute is equal to NSLayoutAttributeWidth or NSLayoutAttributeHeight
|
||||
*/
|
||||
- (BOOL)isSizeAttribute;
|
||||
|
||||
@end
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// MASViewAttribute.m
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 21/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASViewAttribute.h"
|
||||
|
||||
@implementation MASViewAttribute
|
||||
|
||||
- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
|
||||
self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
|
||||
self = [super init];
|
||||
if (!self) return nil;
|
||||
|
||||
_view = view;
|
||||
_item = item;
|
||||
_layoutAttribute = layoutAttribute;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isSizeAttribute {
|
||||
return self.layoutAttribute == NSLayoutAttributeWidth
|
||||
|| self.layoutAttribute == NSLayoutAttributeHeight;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(MASViewAttribute *)viewAttribute {
|
||||
if ([viewAttribute isKindOfClass:self.class]) {
|
||||
return self.view == viewAttribute.view
|
||||
&& self.layoutAttribute == viewAttribute.layoutAttribute;
|
||||
}
|
||||
return [super isEqual:viewAttribute];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return MAS_NSUINTROTATE([self.view hash], MAS_NSUINT_BIT / 2) ^ self.layoutAttribute;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// MASViewConstraint.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 20/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASViewAttribute.h"
|
||||
#import "MASConstraint.h"
|
||||
#import "MASLayoutConstraint.h"
|
||||
#import "MASUtilities.h"
|
||||
|
||||
/**
|
||||
* A single constraint.
|
||||
* Contains the attributes neccessary for creating a NSLayoutConstraint and adding it to the appropriate view
|
||||
*/
|
||||
@interface MASViewConstraint : MASConstraint <NSCopying>
|
||||
|
||||
/**
|
||||
* First item/view and first attribute of the NSLayoutConstraint
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *firstViewAttribute;
|
||||
|
||||
/**
|
||||
* Second item/view and second attribute of the NSLayoutConstraint
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *secondViewAttribute;
|
||||
|
||||
/**
|
||||
* initialises the MASViewConstraint with the first part of the equation
|
||||
*
|
||||
* @param firstViewAttribute view.mas_left, view.mas_width etc.
|
||||
*
|
||||
* @return a new view constraint
|
||||
*/
|
||||
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute;
|
||||
|
||||
/**
|
||||
* Returns all MASViewConstraints installed with this view as a first item.
|
||||
*
|
||||
* @param view A view to retrieve constraints for.
|
||||
*
|
||||
* @return An array of MASViewConstraints.
|
||||
*/
|
||||
+ (NSArray *)installedConstraintsForView:(MAS_VIEW *)view;
|
||||
|
||||
@end
|
||||
@@ -1,401 +0,0 @@
|
||||
//
|
||||
// MASViewConstraint.m
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 20/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASViewConstraint.h"
|
||||
#import "MASConstraint+Private.h"
|
||||
#import "MASCompositeConstraint.h"
|
||||
#import "MASLayoutConstraint.h"
|
||||
#import "View+MASAdditions.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface MAS_VIEW (MASConstraints)
|
||||
|
||||
@property (nonatomic, readonly) NSMutableSet *mas_installedConstraints;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MAS_VIEW (MASConstraints)
|
||||
|
||||
static char kInstalledConstraintsKey;
|
||||
|
||||
- (NSMutableSet *)mas_installedConstraints {
|
||||
NSMutableSet *constraints = objc_getAssociatedObject(self, &kInstalledConstraintsKey);
|
||||
if (!constraints) {
|
||||
constraints = [NSMutableSet set];
|
||||
objc_setAssociatedObject(self, &kInstalledConstraintsKey, constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface MASViewConstraint ()
|
||||
|
||||
@property (nonatomic, strong, readwrite) MASViewAttribute *secondViewAttribute;
|
||||
@property (nonatomic, weak) MAS_VIEW *installedView;
|
||||
@property (nonatomic, weak) MASLayoutConstraint *layoutConstraint;
|
||||
@property (nonatomic, assign) NSLayoutRelation layoutRelation;
|
||||
@property (nonatomic, assign) MASLayoutPriority layoutPriority;
|
||||
@property (nonatomic, assign) CGFloat layoutMultiplier;
|
||||
@property (nonatomic, assign) CGFloat layoutConstant;
|
||||
@property (nonatomic, assign) BOOL hasLayoutRelation;
|
||||
@property (nonatomic, strong) id mas_key;
|
||||
@property (nonatomic, assign) BOOL useAnimator;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MASViewConstraint
|
||||
|
||||
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
|
||||
self = [super init];
|
||||
if (!self) return nil;
|
||||
|
||||
_firstViewAttribute = firstViewAttribute;
|
||||
self.layoutPriority = MASLayoutPriorityRequired;
|
||||
self.layoutMultiplier = 1;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - NSCoping
|
||||
|
||||
- (id)copyWithZone:(NSZone __unused *)zone {
|
||||
MASViewConstraint *constraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:self.firstViewAttribute];
|
||||
constraint.layoutConstant = self.layoutConstant;
|
||||
constraint.layoutRelation = self.layoutRelation;
|
||||
constraint.layoutPriority = self.layoutPriority;
|
||||
constraint.layoutMultiplier = self.layoutMultiplier;
|
||||
constraint.delegate = self.delegate;
|
||||
return constraint;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
+ (NSArray *)installedConstraintsForView:(MAS_VIEW *)view {
|
||||
return [view.mas_installedConstraints allObjects];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)setLayoutConstant:(CGFloat)layoutConstant {
|
||||
_layoutConstant = layoutConstant;
|
||||
|
||||
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
|
||||
if (self.useAnimator) {
|
||||
[self.layoutConstraint.animator setConstant:layoutConstant];
|
||||
} else {
|
||||
self.layoutConstraint.constant = layoutConstant;
|
||||
}
|
||||
#else
|
||||
self.layoutConstraint.constant = layoutConstant;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
|
||||
_layoutRelation = layoutRelation;
|
||||
self.hasLayoutRelation = YES;
|
||||
}
|
||||
|
||||
- (BOOL)supportsActiveProperty {
|
||||
return [self.layoutConstraint respondsToSelector:@selector(isActive)];
|
||||
}
|
||||
|
||||
- (BOOL)isActive {
|
||||
BOOL active = YES;
|
||||
if ([self supportsActiveProperty]) {
|
||||
active = [self.layoutConstraint isActive];
|
||||
}
|
||||
|
||||
return active;
|
||||
}
|
||||
|
||||
- (BOOL)hasBeenInstalled {
|
||||
return (self.layoutConstraint != nil) && [self isActive];
|
||||
}
|
||||
|
||||
- (void)setSecondViewAttribute:(id)secondViewAttribute {
|
||||
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
|
||||
[self setLayoutConstantWithValue:secondViewAttribute];
|
||||
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
|
||||
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
|
||||
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
|
||||
_secondViewAttribute = secondViewAttribute;
|
||||
} else {
|
||||
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutConstraint multiplier proxies
|
||||
|
||||
- (MASConstraint * (^)(CGFloat))multipliedBy {
|
||||
return ^id(CGFloat multiplier) {
|
||||
NSAssert(!self.hasBeenInstalled,
|
||||
@"Cannot modify constraint multiplier after it has been installed");
|
||||
|
||||
self.layoutMultiplier = multiplier;
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
- (MASConstraint * (^)(CGFloat))dividedBy {
|
||||
return ^id(CGFloat divider) {
|
||||
NSAssert(!self.hasBeenInstalled,
|
||||
@"Cannot modify constraint multiplier after it has been installed");
|
||||
|
||||
self.layoutMultiplier = 1.0/divider;
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - MASLayoutPriority proxy
|
||||
|
||||
- (MASConstraint * (^)(MASLayoutPriority))priority {
|
||||
return ^id(MASLayoutPriority priority) {
|
||||
NSAssert(!self.hasBeenInstalled,
|
||||
@"Cannot modify constraint priority after it has been installed");
|
||||
|
||||
self.layoutPriority = priority;
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutRelation proxy
|
||||
|
||||
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
|
||||
return ^id(id attribute, NSLayoutRelation relation) {
|
||||
if ([attribute isKindOfClass:NSArray.class]) {
|
||||
NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
|
||||
NSMutableArray *children = NSMutableArray.new;
|
||||
for (id attr in attribute) {
|
||||
MASViewConstraint *viewConstraint = [self copy];
|
||||
viewConstraint.layoutRelation = relation;
|
||||
viewConstraint.secondViewAttribute = attr;
|
||||
[children addObject:viewConstraint];
|
||||
}
|
||||
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
|
||||
compositeConstraint.delegate = self.delegate;
|
||||
[self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
|
||||
return compositeConstraint;
|
||||
} else {
|
||||
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
|
||||
self.layoutRelation = relation;
|
||||
self.secondViewAttribute = attribute;
|
||||
return self;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - Semantic properties
|
||||
|
||||
- (MASConstraint *)with {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (MASConstraint *)and {
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - attribute chaining
|
||||
|
||||
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
|
||||
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
|
||||
|
||||
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
|
||||
}
|
||||
|
||||
#pragma mark - Animator proxy
|
||||
|
||||
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
|
||||
|
||||
- (MASConstraint *)animator {
|
||||
self.useAnimator = YES;
|
||||
return self;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark - debug helpers
|
||||
|
||||
- (MASConstraint * (^)(id))key {
|
||||
return ^id(id key) {
|
||||
self.mas_key = key;
|
||||
return self;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutConstraint constant setters
|
||||
|
||||
- (void)setInsets:(MASEdgeInsets)insets {
|
||||
NSLayoutAttribute layoutAttribute = self.firstViewAttribute.layoutAttribute;
|
||||
switch (layoutAttribute) {
|
||||
case NSLayoutAttributeLeft:
|
||||
case NSLayoutAttributeLeading:
|
||||
self.layoutConstant = insets.left;
|
||||
break;
|
||||
case NSLayoutAttributeTop:
|
||||
self.layoutConstant = insets.top;
|
||||
break;
|
||||
case NSLayoutAttributeBottom:
|
||||
self.layoutConstant = -insets.bottom;
|
||||
break;
|
||||
case NSLayoutAttributeRight:
|
||||
case NSLayoutAttributeTrailing:
|
||||
self.layoutConstant = -insets.right;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInset:(CGFloat)inset {
|
||||
[self setInsets:(MASEdgeInsets){.top = inset, .left = inset, .bottom = inset, .right = inset}];
|
||||
}
|
||||
|
||||
- (void)setOffset:(CGFloat)offset {
|
||||
self.layoutConstant = offset;
|
||||
}
|
||||
|
||||
- (void)setSizeOffset:(CGSize)sizeOffset {
|
||||
NSLayoutAttribute layoutAttribute = self.firstViewAttribute.layoutAttribute;
|
||||
switch (layoutAttribute) {
|
||||
case NSLayoutAttributeWidth:
|
||||
self.layoutConstant = sizeOffset.width;
|
||||
break;
|
||||
case NSLayoutAttributeHeight:
|
||||
self.layoutConstant = sizeOffset.height;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setCenterOffset:(CGPoint)centerOffset {
|
||||
NSLayoutAttribute layoutAttribute = self.firstViewAttribute.layoutAttribute;
|
||||
switch (layoutAttribute) {
|
||||
case NSLayoutAttributeCenterX:
|
||||
self.layoutConstant = centerOffset.x;
|
||||
break;
|
||||
case NSLayoutAttributeCenterY:
|
||||
self.layoutConstant = centerOffset.y;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - MASConstraint
|
||||
|
||||
- (void)activate {
|
||||
[self install];
|
||||
}
|
||||
|
||||
- (void)deactivate {
|
||||
[self uninstall];
|
||||
}
|
||||
|
||||
- (void)install {
|
||||
if (self.hasBeenInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self supportsActiveProperty] && self.layoutConstraint) {
|
||||
self.layoutConstraint.active = YES;
|
||||
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
|
||||
return;
|
||||
}
|
||||
|
||||
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
|
||||
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
|
||||
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
|
||||
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
|
||||
|
||||
// alignment attributes must have a secondViewAttribute
|
||||
// therefore we assume that is refering to superview
|
||||
// eg make.left.equalTo(@10)
|
||||
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
|
||||
secondLayoutItem = self.firstViewAttribute.view.superview;
|
||||
secondLayoutAttribute = firstLayoutAttribute;
|
||||
}
|
||||
|
||||
MASLayoutConstraint *layoutConstraint
|
||||
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
|
||||
attribute:firstLayoutAttribute
|
||||
relatedBy:self.layoutRelation
|
||||
toItem:secondLayoutItem
|
||||
attribute:secondLayoutAttribute
|
||||
multiplier:self.layoutMultiplier
|
||||
constant:self.layoutConstant];
|
||||
|
||||
layoutConstraint.priority = self.layoutPriority;
|
||||
layoutConstraint.mas_key = self.mas_key;
|
||||
|
||||
if (self.secondViewAttribute.view) {
|
||||
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
|
||||
NSAssert(closestCommonSuperview,
|
||||
@"couldn't find a common superview for %@ and %@",
|
||||
self.firstViewAttribute.view, self.secondViewAttribute.view);
|
||||
self.installedView = closestCommonSuperview;
|
||||
} else if (self.firstViewAttribute.isSizeAttribute) {
|
||||
self.installedView = self.firstViewAttribute.view;
|
||||
} else {
|
||||
self.installedView = self.firstViewAttribute.view.superview;
|
||||
}
|
||||
|
||||
|
||||
MASLayoutConstraint *existingConstraint = nil;
|
||||
if (self.updateExisting) {
|
||||
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
|
||||
}
|
||||
if (existingConstraint) {
|
||||
// just update the constant
|
||||
existingConstraint.constant = layoutConstraint.constant;
|
||||
self.layoutConstraint = existingConstraint;
|
||||
} else {
|
||||
[self.installedView addConstraint:layoutConstraint];
|
||||
self.layoutConstraint = layoutConstraint;
|
||||
[firstLayoutItem.mas_installedConstraints addObject:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
|
||||
// check if any constraints are the same apart from the only mutable property constant
|
||||
|
||||
// go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints
|
||||
// and they are likely to be added first.
|
||||
for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
|
||||
if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
|
||||
if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
|
||||
if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
|
||||
if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
|
||||
if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
|
||||
if (existingConstraint.relation != layoutConstraint.relation) continue;
|
||||
if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
|
||||
if (existingConstraint.priority != layoutConstraint.priority) continue;
|
||||
|
||||
return (id)existingConstraint;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)uninstall {
|
||||
if ([self supportsActiveProperty]) {
|
||||
self.layoutConstraint.active = NO;
|
||||
[self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
|
||||
return;
|
||||
}
|
||||
|
||||
[self.installedView removeConstraint:self.layoutConstraint];
|
||||
self.layoutConstraint = nil;
|
||||
self.installedView = nil;
|
||||
|
||||
[self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// Masonry.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 20/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for Masonry.
|
||||
FOUNDATION_EXPORT double MasonryVersionNumber;
|
||||
|
||||
//! Project version string for Masonry.
|
||||
FOUNDATION_EXPORT const unsigned char MasonryVersionString[];
|
||||
|
||||
#import "MASUtilities.h"
|
||||
#import "View+MASAdditions.h"
|
||||
#import "View+MASShorthandAdditions.h"
|
||||
#import "ViewController+MASAdditions.h"
|
||||
#import "NSArray+MASAdditions.h"
|
||||
#import "NSArray+MASShorthandAdditions.h"
|
||||
#import "MASConstraint.h"
|
||||
#import "MASCompositeConstraint.h"
|
||||
#import "MASViewAttribute.h"
|
||||
#import "MASViewConstraint.h"
|
||||
#import "MASConstraintMaker.h"
|
||||
#import "MASLayoutConstraint.h"
|
||||
#import "NSLayoutConstraint+MASDebugAdditions.h"
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// NSArray+MASAdditions.h
|
||||
//
|
||||
//
|
||||
// Created by Daniel Hammond on 11/26/13.
|
||||
//
|
||||
//
|
||||
|
||||
#import "MASUtilities.h"
|
||||
#import "MASConstraintMaker.h"
|
||||
#import "MASViewAttribute.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, MASAxisType) {
|
||||
MASAxisTypeHorizontal,
|
||||
MASAxisTypeVertical
|
||||
};
|
||||
|
||||
@interface NSArray (MASAdditions)
|
||||
|
||||
/**
|
||||
* Creates a MASConstraintMaker with each view in the callee.
|
||||
* Any constraints defined are added to the view or the appropriate superview once the block has finished executing on each view
|
||||
*
|
||||
* @param block scope within which you can build up the constraints which you wish to apply to each view.
|
||||
*
|
||||
* @return Array of created MASConstraints
|
||||
*/
|
||||
- (NSArray *)mas_makeConstraints:(void (NS_NOESCAPE ^)(MASConstraintMaker *make))block;
|
||||
|
||||
/**
|
||||
* Creates a MASConstraintMaker with each view in the callee.
|
||||
* Any constraints defined are added to each view or the appropriate superview once the block has finished executing on each view.
|
||||
* If an existing constraint exists then it will be updated instead.
|
||||
*
|
||||
* @param block scope within which you can build up the constraints which you wish to apply to each view.
|
||||
*
|
||||
* @return Array of created/updated MASConstraints
|
||||
*/
|
||||
- (NSArray *)mas_updateConstraints:(void (NS_NOESCAPE ^)(MASConstraintMaker *make))block;
|
||||
|
||||
/**
|
||||
* Creates a MASConstraintMaker with each view in the callee.
|
||||
* Any constraints defined are added to each view or the appropriate superview once the block has finished executing on each view.
|
||||
* All constraints previously installed for the views will be removed.
|
||||
*
|
||||
* @param block scope within which you can build up the constraints which you wish to apply to each view.
|
||||
*
|
||||
* @return Array of created/updated MASConstraints
|
||||
*/
|
||||
- (NSArray *)mas_remakeConstraints:(void (NS_NOESCAPE ^)(MASConstraintMaker *make))block;
|
||||
|
||||
/**
|
||||
* distribute with fixed spacing
|
||||
*
|
||||
* @param axisType which axis to distribute items along
|
||||
* @param fixedSpacing the spacing between each item
|
||||
* @param leadSpacing the spacing before the first item and the container
|
||||
* @param tailSpacing the spacing after the last item and the container
|
||||
*/
|
||||
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedSpacing:(CGFloat)fixedSpacing leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing;
|
||||
|
||||
/**
|
||||
* distribute with fixed item size
|
||||
*
|
||||
* @param axisType which axis to distribute items along
|
||||
* @param fixedItemLength the fixed length of each item
|
||||
* @param leadSpacing the spacing before the first item and the container
|
||||
* @param tailSpacing the spacing after the last item and the container
|
||||
*/
|
||||
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedItemLength:(CGFloat)fixedItemLength leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing;
|
||||
|
||||
@end
|
||||
@@ -1,162 +0,0 @@
|
||||
//
|
||||
// NSArray+MASAdditions.m
|
||||
//
|
||||
//
|
||||
// Created by Daniel Hammond on 11/26/13.
|
||||
//
|
||||
//
|
||||
|
||||
#import "NSArray+MASAdditions.h"
|
||||
#import "View+MASAdditions.h"
|
||||
|
||||
@implementation NSArray (MASAdditions)
|
||||
|
||||
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
for (MAS_VIEW *view in self) {
|
||||
NSAssert([view isKindOfClass:[MAS_VIEW class]], @"All objects in the array must be views");
|
||||
[constraints addObjectsFromArray:[view mas_makeConstraints:block]];
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
|
||||
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
for (MAS_VIEW *view in self) {
|
||||
NSAssert([view isKindOfClass:[MAS_VIEW class]], @"All objects in the array must be views");
|
||||
[constraints addObjectsFromArray:[view mas_updateConstraints:block]];
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
|
||||
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
|
||||
NSMutableArray *constraints = [NSMutableArray array];
|
||||
for (MAS_VIEW *view in self) {
|
||||
NSAssert([view isKindOfClass:[MAS_VIEW class]], @"All objects in the array must be views");
|
||||
[constraints addObjectsFromArray:[view mas_remakeConstraints:block]];
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
|
||||
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedSpacing:(CGFloat)fixedSpacing leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing {
|
||||
if (self.count < 2) {
|
||||
NSAssert(self.count>1,@"views to distribute need to bigger than one");
|
||||
return;
|
||||
}
|
||||
|
||||
MAS_VIEW *tempSuperView = [self mas_commonSuperviewOfViews];
|
||||
if (axisType == MASAxisTypeHorizontal) {
|
||||
MAS_VIEW *prev;
|
||||
for (int i = 0; i < self.count; i++) {
|
||||
MAS_VIEW *v = self[i];
|
||||
[v mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
if (prev) {
|
||||
make.width.equalTo(prev);
|
||||
make.left.equalTo(prev.mas_right).offset(fixedSpacing);
|
||||
if (i == self.count - 1) {//last one
|
||||
make.right.equalTo(tempSuperView).offset(-tailSpacing);
|
||||
}
|
||||
}
|
||||
else {//first one
|
||||
make.left.equalTo(tempSuperView).offset(leadSpacing);
|
||||
}
|
||||
|
||||
}];
|
||||
prev = v;
|
||||
}
|
||||
}
|
||||
else {
|
||||
MAS_VIEW *prev;
|
||||
for (int i = 0; i < self.count; i++) {
|
||||
MAS_VIEW *v = self[i];
|
||||
[v mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
if (prev) {
|
||||
make.height.equalTo(prev);
|
||||
make.top.equalTo(prev.mas_bottom).offset(fixedSpacing);
|
||||
if (i == self.count - 1) {//last one
|
||||
make.bottom.equalTo(tempSuperView).offset(-tailSpacing);
|
||||
}
|
||||
}
|
||||
else {//first one
|
||||
make.top.equalTo(tempSuperView).offset(leadSpacing);
|
||||
}
|
||||
|
||||
}];
|
||||
prev = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedItemLength:(CGFloat)fixedItemLength leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing {
|
||||
if (self.count < 2) {
|
||||
NSAssert(self.count>1,@"views to distribute need to bigger than one");
|
||||
return;
|
||||
}
|
||||
|
||||
MAS_VIEW *tempSuperView = [self mas_commonSuperviewOfViews];
|
||||
if (axisType == MASAxisTypeHorizontal) {
|
||||
MAS_VIEW *prev;
|
||||
for (int i = 0; i < self.count; i++) {
|
||||
MAS_VIEW *v = self[i];
|
||||
[v mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.equalTo(@(fixedItemLength));
|
||||
if (prev) {
|
||||
if (i == self.count - 1) {//last one
|
||||
make.right.equalTo(tempSuperView).offset(-tailSpacing);
|
||||
}
|
||||
else {
|
||||
CGFloat offset = (1-(i/((CGFloat)self.count-1)))*(fixedItemLength+leadSpacing)-i*tailSpacing/(((CGFloat)self.count-1));
|
||||
make.right.equalTo(tempSuperView).multipliedBy(i/((CGFloat)self.count-1)).with.offset(offset);
|
||||
}
|
||||
}
|
||||
else {//first one
|
||||
make.left.equalTo(tempSuperView).offset(leadSpacing);
|
||||
}
|
||||
}];
|
||||
prev = v;
|
||||
}
|
||||
}
|
||||
else {
|
||||
MAS_VIEW *prev;
|
||||
for (int i = 0; i < self.count; i++) {
|
||||
MAS_VIEW *v = self[i];
|
||||
[v mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.height.equalTo(@(fixedItemLength));
|
||||
if (prev) {
|
||||
if (i == self.count - 1) {//last one
|
||||
make.bottom.equalTo(tempSuperView).offset(-tailSpacing);
|
||||
}
|
||||
else {
|
||||
CGFloat offset = (1-(i/((CGFloat)self.count-1)))*(fixedItemLength+leadSpacing)-i*tailSpacing/(((CGFloat)self.count-1));
|
||||
make.bottom.equalTo(tempSuperView).multipliedBy(i/((CGFloat)self.count-1)).with.offset(offset);
|
||||
}
|
||||
}
|
||||
else {//first one
|
||||
make.top.equalTo(tempSuperView).offset(leadSpacing);
|
||||
}
|
||||
}];
|
||||
prev = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (MAS_VIEW *)mas_commonSuperviewOfViews
|
||||
{
|
||||
MAS_VIEW *commonSuperview = nil;
|
||||
MAS_VIEW *previousView = nil;
|
||||
for (id object in self) {
|
||||
if ([object isKindOfClass:[MAS_VIEW class]]) {
|
||||
MAS_VIEW *view = (MAS_VIEW *)object;
|
||||
if (previousView) {
|
||||
commonSuperview = [view mas_closestCommonSuperview:commonSuperview];
|
||||
} else {
|
||||
commonSuperview = view;
|
||||
}
|
||||
previousView = view;
|
||||
}
|
||||
}
|
||||
NSAssert(commonSuperview, @"Can't constrain views that do not share a common superview. Make sure that all the views in this array have been added into the same view hierarchy.");
|
||||
return commonSuperview;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// NSArray+MASShorthandAdditions.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 22/07/13.
|
||||
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSArray+MASAdditions.h"
|
||||
|
||||
#ifdef MAS_SHORTHAND
|
||||
|
||||
/**
|
||||
* Shorthand array additions without the 'mas_' prefixes,
|
||||
* only enabled if MAS_SHORTHAND is defined
|
||||
*/
|
||||
@interface NSArray (MASShorthandAdditions)
|
||||
|
||||
- (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *make))block;
|
||||
- (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *make))block;
|
||||
- (NSArray *)remakeConstraints:(void(^)(MASConstraintMaker *make))block;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSArray (MASShorthandAdditions)
|
||||
|
||||
- (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *))block {
|
||||
return [self mas_makeConstraints:block];
|
||||
}
|
||||
|
||||
- (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *))block {
|
||||
return [self mas_updateConstraints:block];
|
||||
}
|
||||
|
||||
- (NSArray *)remakeConstraints:(void(^)(MASConstraintMaker *))block {
|
||||
return [self mas_remakeConstraints:block];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// NSLayoutConstraint+MASDebugAdditions.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 3/08/13.
|
||||
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASUtilities.h"
|
||||
|
||||
/**
|
||||
* makes debug and log output of NSLayoutConstraints more readable
|
||||
*/
|
||||
@interface NSLayoutConstraint (MASDebugAdditions)
|
||||
|
||||
@end
|
||||
@@ -1,146 +0,0 @@
|
||||
//
|
||||
// NSLayoutConstraint+MASDebugAdditions.m
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 3/08/13.
|
||||
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSLayoutConstraint+MASDebugAdditions.h"
|
||||
#import "MASConstraint.h"
|
||||
#import "MASLayoutConstraint.h"
|
||||
|
||||
@implementation NSLayoutConstraint (MASDebugAdditions)
|
||||
|
||||
#pragma mark - description maps
|
||||
|
||||
+ (NSDictionary *)layoutRelationDescriptionsByValue {
|
||||
static dispatch_once_t once;
|
||||
static NSDictionary *descriptionMap;
|
||||
dispatch_once(&once, ^{
|
||||
descriptionMap = @{
|
||||
@(NSLayoutRelationEqual) : @"==",
|
||||
@(NSLayoutRelationGreaterThanOrEqual) : @">=",
|
||||
@(NSLayoutRelationLessThanOrEqual) : @"<=",
|
||||
};
|
||||
});
|
||||
return descriptionMap;
|
||||
}
|
||||
|
||||
+ (NSDictionary *)layoutAttributeDescriptionsByValue {
|
||||
static dispatch_once_t once;
|
||||
static NSDictionary *descriptionMap;
|
||||
dispatch_once(&once, ^{
|
||||
descriptionMap = @{
|
||||
@(NSLayoutAttributeTop) : @"top",
|
||||
@(NSLayoutAttributeLeft) : @"left",
|
||||
@(NSLayoutAttributeBottom) : @"bottom",
|
||||
@(NSLayoutAttributeRight) : @"right",
|
||||
@(NSLayoutAttributeLeading) : @"leading",
|
||||
@(NSLayoutAttributeTrailing) : @"trailing",
|
||||
@(NSLayoutAttributeWidth) : @"width",
|
||||
@(NSLayoutAttributeHeight) : @"height",
|
||||
@(NSLayoutAttributeCenterX) : @"centerX",
|
||||
@(NSLayoutAttributeCenterY) : @"centerY",
|
||||
@(NSLayoutAttributeBaseline) : @"baseline",
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
@(NSLayoutAttributeFirstBaseline) : @"firstBaseline",
|
||||
@(NSLayoutAttributeLastBaseline) : @"lastBaseline",
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
@(NSLayoutAttributeLeftMargin) : @"leftMargin",
|
||||
@(NSLayoutAttributeRightMargin) : @"rightMargin",
|
||||
@(NSLayoutAttributeTopMargin) : @"topMargin",
|
||||
@(NSLayoutAttributeBottomMargin) : @"bottomMargin",
|
||||
@(NSLayoutAttributeLeadingMargin) : @"leadingMargin",
|
||||
@(NSLayoutAttributeTrailingMargin) : @"trailingMargin",
|
||||
@(NSLayoutAttributeCenterXWithinMargins) : @"centerXWithinMargins",
|
||||
@(NSLayoutAttributeCenterYWithinMargins) : @"centerYWithinMargins",
|
||||
#endif
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
return descriptionMap;
|
||||
}
|
||||
|
||||
|
||||
+ (NSDictionary *)layoutPriorityDescriptionsByValue {
|
||||
static dispatch_once_t once;
|
||||
static NSDictionary *descriptionMap;
|
||||
dispatch_once(&once, ^{
|
||||
#if TARGET_OS_IPHONE || TARGET_OS_TV
|
||||
descriptionMap = @{
|
||||
@(MASLayoutPriorityDefaultHigh) : @"high",
|
||||
@(MASLayoutPriorityDefaultLow) : @"low",
|
||||
@(MASLayoutPriorityDefaultMedium) : @"medium",
|
||||
@(MASLayoutPriorityRequired) : @"required",
|
||||
@(MASLayoutPriorityFittingSizeLevel) : @"fitting size",
|
||||
};
|
||||
#elif TARGET_OS_MAC
|
||||
descriptionMap = @{
|
||||
@(MASLayoutPriorityDefaultHigh) : @"high",
|
||||
@(MASLayoutPriorityDragThatCanResizeWindow) : @"drag can resize window",
|
||||
@(MASLayoutPriorityDefaultMedium) : @"medium",
|
||||
@(MASLayoutPriorityWindowSizeStayPut) : @"window size stay put",
|
||||
@(MASLayoutPriorityDragThatCannotResizeWindow) : @"drag cannot resize window",
|
||||
@(MASLayoutPriorityDefaultLow) : @"low",
|
||||
@(MASLayoutPriorityFittingSizeCompression) : @"fitting size",
|
||||
@(MASLayoutPriorityRequired) : @"required",
|
||||
};
|
||||
#endif
|
||||
});
|
||||
return descriptionMap;
|
||||
}
|
||||
|
||||
#pragma mark - description override
|
||||
|
||||
+ (NSString *)descriptionForObject:(id)obj {
|
||||
if ([obj respondsToSelector:@selector(mas_key)] && [obj mas_key]) {
|
||||
return [NSString stringWithFormat:@"%@:%@", [obj class], [obj mas_key]];
|
||||
}
|
||||
return [NSString stringWithFormat:@"%@:%p", [obj class], obj];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
NSMutableString *description = [[NSMutableString alloc] initWithString:@"<"];
|
||||
|
||||
[description appendString:[self.class descriptionForObject:self]];
|
||||
|
||||
[description appendFormat:@" %@", [self.class descriptionForObject:self.firstItem]];
|
||||
if (self.firstAttribute != NSLayoutAttributeNotAnAttribute) {
|
||||
[description appendFormat:@".%@", self.class.layoutAttributeDescriptionsByValue[@(self.firstAttribute)]];
|
||||
}
|
||||
|
||||
[description appendFormat:@" %@", self.class.layoutRelationDescriptionsByValue[@(self.relation)]];
|
||||
|
||||
if (self.secondItem) {
|
||||
[description appendFormat:@" %@", [self.class descriptionForObject:self.secondItem]];
|
||||
}
|
||||
if (self.secondAttribute != NSLayoutAttributeNotAnAttribute) {
|
||||
[description appendFormat:@".%@", self.class.layoutAttributeDescriptionsByValue[@(self.secondAttribute)]];
|
||||
}
|
||||
|
||||
if (self.multiplier != 1) {
|
||||
[description appendFormat:@" * %g", self.multiplier];
|
||||
}
|
||||
|
||||
if (self.secondAttribute == NSLayoutAttributeNotAnAttribute) {
|
||||
[description appendFormat:@" %g", self.constant];
|
||||
} else {
|
||||
if (self.constant) {
|
||||
[description appendFormat:@" %@ %g", (self.constant < 0 ? @"-" : @"+"), ABS(self.constant)];
|
||||
}
|
||||
}
|
||||
|
||||
if (self.priority != MASLayoutPriorityRequired) {
|
||||
[description appendFormat:@" ^%@", self.class.layoutPriorityDescriptionsByValue[@(self.priority)] ?: [NSNumber numberWithDouble:self.priority]];
|
||||
}
|
||||
|
||||
[description appendString:@">"];
|
||||
return description;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,111 +0,0 @@
|
||||
//
|
||||
// UIView+MASAdditions.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 20/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MASUtilities.h"
|
||||
#import "MASConstraintMaker.h"
|
||||
#import "MASViewAttribute.h"
|
||||
|
||||
/**
|
||||
* Provides constraint maker block
|
||||
* and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs
|
||||
*/
|
||||
@interface MAS_VIEW (MASAdditions)
|
||||
|
||||
/**
|
||||
* following properties return a new MASViewAttribute with current view and appropriate NSLayoutAttribute
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_left;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_right;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_leading;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_trailing;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_width;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_height;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerX;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerY;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_baseline;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *(^mas_attribute)(NSLayoutAttribute attr);
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_firstBaseline;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_lastBaseline;
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_leftMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_rightMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_leadingMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_trailingMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerXWithinMargins;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerYWithinMargins;
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000)
|
||||
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideTop API_AVAILABLE(ios(11.0),tvos(11.0));
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideBottom API_AVAILABLE(ios(11.0),tvos(11.0));
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideLeft API_AVAILABLE(ios(11.0),tvos(11.0));
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideRight API_AVAILABLE(ios(11.0),tvos(11.0));
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* a key to associate with this view
|
||||
*/
|
||||
@property (nonatomic, strong) id mas_key;
|
||||
|
||||
/**
|
||||
* Finds the closest common superview between this view and another view
|
||||
*
|
||||
* @param view other view
|
||||
*
|
||||
* @return returns nil if common superview could not be found
|
||||
*/
|
||||
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view;
|
||||
|
||||
/**
|
||||
* Creates a MASConstraintMaker with the callee view.
|
||||
* Any constraints defined are added to the view or the appropriate superview once the block has finished executing
|
||||
*
|
||||
* @param block scope within which you can build up the constraints which you wish to apply to the view.
|
||||
*
|
||||
* @return Array of created MASConstraints
|
||||
*/
|
||||
- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
|
||||
|
||||
/**
|
||||
* Creates a MASConstraintMaker with the callee view.
|
||||
* Any constraints defined are added to the view or the appropriate superview once the block has finished executing.
|
||||
* If an existing constraint exists then it will be updated instead.
|
||||
*
|
||||
* @param block scope within which you can build up the constraints which you wish to apply to the view.
|
||||
*
|
||||
* @return Array of created/updated MASConstraints
|
||||
*/
|
||||
- (NSArray *)mas_updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
|
||||
|
||||
/**
|
||||
* Creates a MASConstraintMaker with the callee view.
|
||||
* Any constraints defined are added to the view or the appropriate superview once the block has finished executing.
|
||||
* All constraints previously installed for the view will be removed.
|
||||
*
|
||||
* @param block scope within which you can build up the constraints which you wish to apply to the view.
|
||||
*
|
||||
* @return Array of created/updated MASConstraints
|
||||
*/
|
||||
- (NSArray *)mas_remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
|
||||
|
||||
@end
|
||||
@@ -1,186 +0,0 @@
|
||||
//
|
||||
// UIView+MASAdditions.m
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 20/07/13.
|
||||
// Copyright (c) 2013 cloudling. All rights reserved.
|
||||
//
|
||||
|
||||
#import "View+MASAdditions.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation MAS_VIEW (MASAdditions)
|
||||
|
||||
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
|
||||
block(constraintMaker);
|
||||
return [constraintMaker install];
|
||||
}
|
||||
|
||||
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
|
||||
constraintMaker.updateExisting = YES;
|
||||
block(constraintMaker);
|
||||
return [constraintMaker install];
|
||||
}
|
||||
|
||||
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
|
||||
constraintMaker.removeExisting = YES;
|
||||
block(constraintMaker);
|
||||
return [constraintMaker install];
|
||||
}
|
||||
|
||||
#pragma mark - NSLayoutAttribute properties
|
||||
|
||||
- (MASViewAttribute *)mas_left {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeft];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_top {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTop];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_right {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeRight];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_bottom {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeBottom];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_leading {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeading];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_trailing {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTrailing];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_width {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeWidth];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_height {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeHeight];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_centerX {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterX];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_centerY {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterY];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_baseline {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeBaseline];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *(^)(NSLayoutAttribute))mas_attribute
|
||||
{
|
||||
return ^(NSLayoutAttribute attr) {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:attr];
|
||||
};
|
||||
}
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
- (MASViewAttribute *)mas_firstBaseline {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeFirstBaseline];
|
||||
}
|
||||
- (MASViewAttribute *)mas_lastBaseline {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLastBaseline];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
- (MASViewAttribute *)mas_leftMargin {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeftMargin];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_rightMargin {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeRightMargin];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_topMargin {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTopMargin];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_bottomMargin {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeBottomMargin];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_leadingMargin {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeadingMargin];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_trailingMargin {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTrailingMargin];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_centerXWithinMargins {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterXWithinMargins];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_centerYWithinMargins {
|
||||
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterYWithinMargins];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000)
|
||||
|
||||
- (MASViewAttribute *)mas_safeAreaLayoutGuide {
|
||||
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
|
||||
}
|
||||
- (MASViewAttribute *)mas_safeAreaLayoutGuideTop {
|
||||
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeTop];
|
||||
}
|
||||
- (MASViewAttribute *)mas_safeAreaLayoutGuideBottom {
|
||||
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
|
||||
}
|
||||
- (MASViewAttribute *)mas_safeAreaLayoutGuideLeft {
|
||||
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeLeft];
|
||||
}
|
||||
- (MASViewAttribute *)mas_safeAreaLayoutGuideRight {
|
||||
return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeRight];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark - associated properties
|
||||
|
||||
- (id)mas_key {
|
||||
return objc_getAssociatedObject(self, @selector(mas_key));
|
||||
}
|
||||
|
||||
- (void)setMas_key:(id)key {
|
||||
objc_setAssociatedObject(self, @selector(mas_key), key, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
#pragma mark - heirachy
|
||||
|
||||
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
|
||||
MAS_VIEW *closestCommonSuperview = nil;
|
||||
|
||||
MAS_VIEW *secondViewSuperview = view;
|
||||
while (!closestCommonSuperview && secondViewSuperview) {
|
||||
MAS_VIEW *firstViewSuperview = self;
|
||||
while (!closestCommonSuperview && firstViewSuperview) {
|
||||
if (secondViewSuperview == firstViewSuperview) {
|
||||
closestCommonSuperview = secondViewSuperview;
|
||||
}
|
||||
firstViewSuperview = firstViewSuperview.superview;
|
||||
}
|
||||
secondViewSuperview = secondViewSuperview.superview;
|
||||
}
|
||||
return closestCommonSuperview;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,133 +0,0 @@
|
||||
//
|
||||
// UIView+MASShorthandAdditions.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 22/07/13.
|
||||
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
|
||||
//
|
||||
|
||||
#import "View+MASAdditions.h"
|
||||
|
||||
#ifdef MAS_SHORTHAND
|
||||
|
||||
/**
|
||||
* Shorthand view additions without the 'mas_' prefixes,
|
||||
* only enabled if MAS_SHORTHAND is defined
|
||||
*/
|
||||
@interface MAS_VIEW (MASShorthandAdditions)
|
||||
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *left;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *top;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *right;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *bottom;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *leading;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *trailing;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *width;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *height;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *centerX;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *centerY;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *baseline;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *(^attribute)(NSLayoutAttribute attr);
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *firstBaseline;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *lastBaseline;
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *leftMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *rightMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *topMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *bottomMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *leadingMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *trailingMargin;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *centerXWithinMargins;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *centerYWithinMargins;
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000)
|
||||
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideTop API_AVAILABLE(ios(11.0),tvos(11.0));
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideBottom API_AVAILABLE(ios(11.0),tvos(11.0));
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideLeft API_AVAILABLE(ios(11.0),tvos(11.0));
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideRight API_AVAILABLE(ios(11.0),tvos(11.0));
|
||||
|
||||
#endif
|
||||
|
||||
- (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *make))block;
|
||||
- (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *make))block;
|
||||
- (NSArray *)remakeConstraints:(void(^)(MASConstraintMaker *make))block;
|
||||
|
||||
@end
|
||||
|
||||
#define MAS_ATTR_FORWARD(attr) \
|
||||
- (MASViewAttribute *)attr { \
|
||||
return [self mas_##attr]; \
|
||||
}
|
||||
|
||||
@implementation MAS_VIEW (MASShorthandAdditions)
|
||||
|
||||
MAS_ATTR_FORWARD(top);
|
||||
MAS_ATTR_FORWARD(left);
|
||||
MAS_ATTR_FORWARD(bottom);
|
||||
MAS_ATTR_FORWARD(right);
|
||||
MAS_ATTR_FORWARD(leading);
|
||||
MAS_ATTR_FORWARD(trailing);
|
||||
MAS_ATTR_FORWARD(width);
|
||||
MAS_ATTR_FORWARD(height);
|
||||
MAS_ATTR_FORWARD(centerX);
|
||||
MAS_ATTR_FORWARD(centerY);
|
||||
MAS_ATTR_FORWARD(baseline);
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
|
||||
|
||||
MAS_ATTR_FORWARD(firstBaseline);
|
||||
MAS_ATTR_FORWARD(lastBaseline);
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
|
||||
|
||||
MAS_ATTR_FORWARD(leftMargin);
|
||||
MAS_ATTR_FORWARD(rightMargin);
|
||||
MAS_ATTR_FORWARD(topMargin);
|
||||
MAS_ATTR_FORWARD(bottomMargin);
|
||||
MAS_ATTR_FORWARD(leadingMargin);
|
||||
MAS_ATTR_FORWARD(trailingMargin);
|
||||
MAS_ATTR_FORWARD(centerXWithinMargins);
|
||||
MAS_ATTR_FORWARD(centerYWithinMargins);
|
||||
|
||||
#endif
|
||||
|
||||
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000)
|
||||
|
||||
MAS_ATTR_FORWARD(safeAreaLayoutGuideTop);
|
||||
MAS_ATTR_FORWARD(safeAreaLayoutGuideBottom);
|
||||
MAS_ATTR_FORWARD(safeAreaLayoutGuideLeft);
|
||||
MAS_ATTR_FORWARD(safeAreaLayoutGuideRight);
|
||||
|
||||
#endif
|
||||
|
||||
- (MASViewAttribute *(^)(NSLayoutAttribute))attribute {
|
||||
return [self mas_attribute];
|
||||
}
|
||||
|
||||
- (NSArray *)makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
|
||||
return [self mas_makeConstraints:block];
|
||||
}
|
||||
|
||||
- (NSArray *)updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
|
||||
return [self mas_updateConstraints:block];
|
||||
}
|
||||
|
||||
- (NSArray *)remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
|
||||
return [self mas_remakeConstraints:block];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// UIViewController+MASAdditions.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Craig Siemens on 2015-06-23.
|
||||
//
|
||||
//
|
||||
|
||||
#import "MASUtilities.h"
|
||||
#import "MASConstraintMaker.h"
|
||||
#import "MASViewAttribute.h"
|
||||
|
||||
#ifdef MAS_VIEW_CONTROLLER
|
||||
|
||||
@interface MAS_VIEW_CONTROLLER (MASAdditions)
|
||||
|
||||
/**
|
||||
* following properties return a new MASViewAttribute with appropriate UILayoutGuide and NSLayoutAttribute
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuide;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuide;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuideTop;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuideBottom;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuideTop;
|
||||
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuideBottom;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -1,39 +0,0 @@
|
||||
//
|
||||
// UIViewController+MASAdditions.m
|
||||
// Masonry
|
||||
//
|
||||
// Created by Craig Siemens on 2015-06-23.
|
||||
//
|
||||
//
|
||||
|
||||
#import "ViewController+MASAdditions.h"
|
||||
|
||||
#ifdef MAS_VIEW_CONTROLLER
|
||||
|
||||
@implementation MAS_VIEW_CONTROLLER (MASAdditions)
|
||||
|
||||
- (MASViewAttribute *)mas_topLayoutGuide {
|
||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
|
||||
}
|
||||
- (MASViewAttribute *)mas_topLayoutGuideTop {
|
||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeTop];
|
||||
}
|
||||
- (MASViewAttribute *)mas_topLayoutGuideBottom {
|
||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
|
||||
}
|
||||
|
||||
- (MASViewAttribute *)mas_bottomLayoutGuide {
|
||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeTop];
|
||||
}
|
||||
- (MASViewAttribute *)mas_bottomLayoutGuideTop {
|
||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeTop];
|
||||
}
|
||||
- (MASViewAttribute *)mas_bottomLayoutGuideBottom {
|
||||
return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeBottom];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -1,415 +0,0 @@
|
||||
# Masonry [](https://travis-ci.org/SnapKit/Masonry) [](https://coveralls.io/r/SnapKit/Masonry) [](https://github.com/Carthage/Carthage) 
|
||||
|
||||
**Masonry is still actively maintained, we are committed to fixing bugs and merging good quality PRs from the wider community. However if you're using Swift in your project, we recommend using [SnapKit](https://github.com/SnapKit/SnapKit) as it provides better type safety with a simpler API.**
|
||||
|
||||
Masonry is a light-weight layout framework which wraps AutoLayout with a nicer syntax. Masonry has its own layout DSL which provides a chainable way of describing your NSLayoutConstraints which results in layout code that is more concise and readable.
|
||||
Masonry supports iOS and Mac OS X.
|
||||
|
||||
For examples take a look at the **Masonry iOS Examples** project in the Masonry workspace. You will need to run `pod install` after downloading.
|
||||
|
||||
## What's wrong with NSLayoutConstraints?
|
||||
|
||||
Under the hood Auto Layout is a powerful and flexible way of organising and laying out your views. However creating constraints from code is verbose and not very descriptive.
|
||||
Imagine a simple example in which you want to have a view fill its superview but inset by 10 pixels on every side
|
||||
```obj-c
|
||||
UIView *superview = self.view;
|
||||
|
||||
UIView *view1 = [[UIView alloc] init];
|
||||
view1.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
view1.backgroundColor = [UIColor greenColor];
|
||||
[superview addSubview:view1];
|
||||
|
||||
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
|
||||
|
||||
[superview addConstraints:@[
|
||||
|
||||
//view1 constraints
|
||||
[NSLayoutConstraint constraintWithItem:view1
|
||||
attribute:NSLayoutAttributeTop
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:superview
|
||||
attribute:NSLayoutAttributeTop
|
||||
multiplier:1.0
|
||||
constant:padding.top],
|
||||
|
||||
[NSLayoutConstraint constraintWithItem:view1
|
||||
attribute:NSLayoutAttributeLeft
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:superview
|
||||
attribute:NSLayoutAttributeLeft
|
||||
multiplier:1.0
|
||||
constant:padding.left],
|
||||
|
||||
[NSLayoutConstraint constraintWithItem:view1
|
||||
attribute:NSLayoutAttributeBottom
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:superview
|
||||
attribute:NSLayoutAttributeBottom
|
||||
multiplier:1.0
|
||||
constant:-padding.bottom],
|
||||
|
||||
[NSLayoutConstraint constraintWithItem:view1
|
||||
attribute:NSLayoutAttributeRight
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:superview
|
||||
attribute:NSLayoutAttributeRight
|
||||
multiplier:1
|
||||
constant:-padding.right],
|
||||
|
||||
]];
|
||||
```
|
||||
Even with such a simple example the code needed is quite verbose and quickly becomes unreadable when you have more than 2 or 3 views.
|
||||
Another option is to use Visual Format Language (VFL), which is a bit less long winded.
|
||||
However the ASCII type syntax has its own pitfalls and its also a bit harder to animate as `NSLayoutConstraint constraintsWithVisualFormat:` returns an array.
|
||||
|
||||
## Prepare to meet your Maker!
|
||||
|
||||
Heres the same constraints created using MASConstraintMaker
|
||||
|
||||
```obj-c
|
||||
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
|
||||
|
||||
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
|
||||
make.left.equalTo(superview.mas_left).with.offset(padding.left);
|
||||
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
|
||||
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
|
||||
}];
|
||||
```
|
||||
Or even shorter
|
||||
|
||||
```obj-c
|
||||
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(superview).with.insets(padding);
|
||||
}];
|
||||
```
|
||||
|
||||
Also note in the first example we had to add the constraints to the superview `[superview addConstraints:...`.
|
||||
Masonry however will automagically add constraints to the appropriate view.
|
||||
|
||||
Masonry will also call `view1.translatesAutoresizingMaskIntoConstraints = NO;` for you.
|
||||
|
||||
## Not all things are created equal
|
||||
|
||||
> `.equalTo` equivalent to **NSLayoutRelationEqual**
|
||||
|
||||
> `.lessThanOrEqualTo` equivalent to **NSLayoutRelationLessThanOrEqual**
|
||||
|
||||
> `.greaterThanOrEqualTo` equivalent to **NSLayoutRelationGreaterThanOrEqual**
|
||||
|
||||
These three equality constraints accept one argument which can be any of the following:
|
||||
|
||||
#### 1. MASViewAttribute
|
||||
|
||||
```obj-c
|
||||
make.centerX.lessThanOrEqualTo(view2.mas_left);
|
||||
```
|
||||
|
||||
MASViewAttribute | NSLayoutAttribute
|
||||
------------------------- | --------------------------
|
||||
view.mas_left | NSLayoutAttributeLeft
|
||||
view.mas_right | NSLayoutAttributeRight
|
||||
view.mas_top | NSLayoutAttributeTop
|
||||
view.mas_bottom | NSLayoutAttributeBottom
|
||||
view.mas_leading | NSLayoutAttributeLeading
|
||||
view.mas_trailing | NSLayoutAttributeTrailing
|
||||
view.mas_width | NSLayoutAttributeWidth
|
||||
view.mas_height | NSLayoutAttributeHeight
|
||||
view.mas_centerX | NSLayoutAttributeCenterX
|
||||
view.mas_centerY | NSLayoutAttributeCenterY
|
||||
view.mas_baseline | NSLayoutAttributeBaseline
|
||||
|
||||
#### 2. UIView/NSView
|
||||
|
||||
if you want view.left to be greater than or equal to label.left :
|
||||
```obj-c
|
||||
//these two constraints are exactly the same
|
||||
make.left.greaterThanOrEqualTo(label);
|
||||
make.left.greaterThanOrEqualTo(label.mas_left);
|
||||
```
|
||||
|
||||
#### 3. NSNumber
|
||||
|
||||
Auto Layout allows width and height to be set to constant values.
|
||||
if you want to set view to have a minimum and maximum width you could pass a number to the equality blocks:
|
||||
```obj-c
|
||||
//width >= 200 && width <= 400
|
||||
make.width.greaterThanOrEqualTo(@200);
|
||||
make.width.lessThanOrEqualTo(@400)
|
||||
```
|
||||
|
||||
However Auto Layout does not allow alignment attributes such as left, right, centerY etc to be set to constant values.
|
||||
So if you pass a NSNumber for these attributes Masonry will turn these into constraints relative to the view’s superview ie:
|
||||
```obj-c
|
||||
//creates view.left = view.superview.left + 10
|
||||
make.left.lessThanOrEqualTo(@10)
|
||||
```
|
||||
|
||||
Instead of using NSNumber, you can use primitives and structs to build your constraints, like so:
|
||||
```obj-c
|
||||
make.top.mas_equalTo(42);
|
||||
make.height.mas_equalTo(20);
|
||||
make.size.mas_equalTo(CGSizeMake(50, 100));
|
||||
make.edges.mas_equalTo(UIEdgeInsetsMake(10, 0, 10, 0));
|
||||
make.left.mas_equalTo(view).mas_offset(UIEdgeInsetsMake(10, 0, 10, 0));
|
||||
```
|
||||
|
||||
By default, macros which support [autoboxing](https://en.wikipedia.org/wiki/Autoboxing#Autoboxing) are prefixed with `mas_`. Unprefixed versions are available by defining `MAS_SHORTHAND_GLOBALS` before importing Masonry.
|
||||
|
||||
#### 4. NSArray
|
||||
|
||||
An array of a mixture of any of the previous types
|
||||
```obj-c
|
||||
make.height.equalTo(@[view1.mas_height, view2.mas_height]);
|
||||
make.height.equalTo(@[view1, view2]);
|
||||
make.left.equalTo(@[view1, @100, view3.right]);
|
||||
````
|
||||
|
||||
## Learn to prioritize
|
||||
|
||||
> `.priority` allows you to specify an exact priority
|
||||
|
||||
> `.priorityHigh` equivalent to **UILayoutPriorityDefaultHigh**
|
||||
|
||||
> `.priorityMedium` is half way between high and low
|
||||
|
||||
> `.priorityLow` equivalent to **UILayoutPriorityDefaultLow**
|
||||
|
||||
Priorities are can be tacked on to the end of a constraint chain like so:
|
||||
```obj-c
|
||||
make.left.greaterThanOrEqualTo(label.mas_left).with.priorityLow();
|
||||
|
||||
make.top.equalTo(label.mas_top).with.priority(600);
|
||||
```
|
||||
|
||||
## Composition, composition, composition
|
||||
|
||||
Masonry also gives you a few convenience methods which create multiple constraints at the same time. These are called MASCompositeConstraints
|
||||
|
||||
#### edges
|
||||
|
||||
```obj-c
|
||||
// make top, left, bottom, right equal view2
|
||||
make.edges.equalTo(view2);
|
||||
|
||||
// make top = superview.top + 5, left = superview.left + 10,
|
||||
// bottom = superview.bottom - 15, right = superview.right - 20
|
||||
make.edges.equalTo(superview).insets(UIEdgeInsetsMake(5, 10, 15, 20))
|
||||
```
|
||||
|
||||
#### size
|
||||
|
||||
```obj-c
|
||||
// make width and height greater than or equal to titleLabel
|
||||
make.size.greaterThanOrEqualTo(titleLabel)
|
||||
|
||||
// make width = superview.width + 100, height = superview.height - 50
|
||||
make.size.equalTo(superview).sizeOffset(CGSizeMake(100, -50))
|
||||
```
|
||||
|
||||
#### center
|
||||
```obj-c
|
||||
// make centerX and centerY = button1
|
||||
make.center.equalTo(button1)
|
||||
|
||||
// make centerX = superview.centerX - 5, centerY = superview.centerY + 10
|
||||
make.center.equalTo(superview).centerOffset(CGPointMake(-5, 10))
|
||||
```
|
||||
|
||||
You can chain view attributes for increased readability:
|
||||
|
||||
```obj-c
|
||||
// All edges but the top should equal those of the superview
|
||||
make.left.right.and.bottom.equalTo(superview);
|
||||
make.top.equalTo(otherView);
|
||||
```
|
||||
|
||||
## Hold on for dear life
|
||||
|
||||
Sometimes you need modify existing constraints in order to animate or remove/replace constraints.
|
||||
In Masonry there are a few different approaches to updating constraints.
|
||||
|
||||
#### 1. References
|
||||
You can hold on to a reference of a particular constraint by assigning the result of a constraint make expression to a local variable or a class property.
|
||||
You could also reference multiple constraints by storing them away in an array.
|
||||
|
||||
```obj-c
|
||||
// in public/private interface
|
||||
@property (nonatomic, strong) MASConstraint *topConstraint;
|
||||
|
||||
...
|
||||
|
||||
// when making constraints
|
||||
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
self.topConstraint = make.top.equalTo(superview.mas_top).with.offset(padding.top);
|
||||
make.left.equalTo(superview.mas_left).with.offset(padding.left);
|
||||
}];
|
||||
|
||||
...
|
||||
// then later you can call
|
||||
[self.topConstraint uninstall];
|
||||
```
|
||||
|
||||
#### 2. mas_updateConstraints
|
||||
Alternatively if you are only updating the constant value of the constraint you can use the convience method `mas_updateConstraints` instead of `mas_makeConstraints`
|
||||
|
||||
```obj-c
|
||||
// this is Apple's recommended place for adding/updating constraints
|
||||
// this method can get called multiple times in response to setNeedsUpdateConstraints
|
||||
// which can be called by UIKit internally or in your code if you need to trigger an update to your constraints
|
||||
- (void)updateConstraints {
|
||||
[self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.equalTo(self);
|
||||
make.width.equalTo(@(self.buttonSize.width)).priorityLow();
|
||||
make.height.equalTo(@(self.buttonSize.height)).priorityLow();
|
||||
make.width.lessThanOrEqualTo(self);
|
||||
make.height.lessThanOrEqualTo(self);
|
||||
}];
|
||||
|
||||
//according to apple super should be called at end of method
|
||||
[super updateConstraints];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. mas_remakeConstraints
|
||||
`mas_updateConstraints` is useful for updating a set of constraints, but doing anything beyond updating constant values can get exhausting. That's where `mas_remakeConstraints` comes in.
|
||||
|
||||
`mas_remakeConstraints` is similar to `mas_updateConstraints`, but instead of updating constant values, it will remove all of its constraints before installing them again. This lets you provide different constraints without having to keep around references to ones which you want to remove.
|
||||
|
||||
```obj-c
|
||||
- (void)changeButtonPosition {
|
||||
[self.button mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.size.equalTo(self.buttonSize);
|
||||
|
||||
if (topLeft) {
|
||||
make.top.and.left.offset(10);
|
||||
} else {
|
||||
make.bottom.and.right.offset(-10);
|
||||
}
|
||||
}];
|
||||
}
|
||||
```
|
||||
|
||||
You can find more detailed examples of all three approaches in the **Masonry iOS Examples** project.
|
||||
|
||||
## When the ^&*!@ hits the fan!
|
||||
|
||||
Laying out your views doesn't always goto plan. So when things literally go pear shaped, you don't want to be looking at console output like this:
|
||||
|
||||
```obj-c
|
||||
Unable to simultaneously satisfy constraints.....blah blah blah....
|
||||
(
|
||||
"<NSLayoutConstraint:0x7189ac0 V:[UILabel:0x7186980(>=5000)]>",
|
||||
"<NSAutoresizingMaskLayoutConstraint:0x839ea20 h=--& v=--& V:[MASExampleDebuggingView:0x7186560(416)]>",
|
||||
"<NSLayoutConstraint:0x7189c70 UILabel:0x7186980.bottom == MASExampleDebuggingView:0x7186560.bottom - 10>",
|
||||
"<NSLayoutConstraint:0x7189560 V:|-(1)-[UILabel:0x7186980] (Names: '|':MASExampleDebuggingView:0x7186560 )>"
|
||||
)
|
||||
|
||||
Will attempt to recover by breaking constraint
|
||||
<NSLayoutConstraint:0x7189ac0 V:[UILabel:0x7186980(>=5000)]>
|
||||
```
|
||||
|
||||
Masonry adds a category to NSLayoutConstraint which overrides the default implementation of `- (NSString *)description`.
|
||||
Now you can give meaningful names to views and constraints, and also easily pick out the constraints created by Masonry.
|
||||
|
||||
which means your console output can now look like this:
|
||||
|
||||
```obj-c
|
||||
Unable to simultaneously satisfy constraints......blah blah blah....
|
||||
(
|
||||
"<NSAutoresizingMaskLayoutConstraint:0x8887740 MASExampleDebuggingView:superview.height == 416>",
|
||||
"<MASLayoutConstraint:ConstantConstraint UILabel:messageLabel.height >= 5000>",
|
||||
"<MASLayoutConstraint:BottomConstraint UILabel:messageLabel.bottom == MASExampleDebuggingView:superview.bottom - 10>",
|
||||
"<MASLayoutConstraint:ConflictingConstraint[0] UILabel:messageLabel.top == MASExampleDebuggingView:superview.top + 1>"
|
||||
)
|
||||
|
||||
Will attempt to recover by breaking constraint
|
||||
<MASLayoutConstraint:ConstantConstraint UILabel:messageLabel.height >= 5000>
|
||||
```
|
||||
|
||||
For an example of how to set this up take a look at the **Masonry iOS Examples** project in the Masonry workspace.
|
||||
|
||||
## Where should I create my constraints?
|
||||
|
||||
```objc
|
||||
@implementation DIYCustomView
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (!self) return nil;
|
||||
|
||||
// --- Create your views here ---
|
||||
self.button = [[UIButton alloc] init];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// tell UIKit that you are using AutoLayout
|
||||
+ (BOOL)requiresConstraintBasedLayout {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// this is Apple's recommended place for adding/updating constraints
|
||||
- (void)updateConstraints {
|
||||
|
||||
// --- remake/update constraints here
|
||||
[self.button remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.equalTo(@(self.buttonSize.width));
|
||||
make.height.equalTo(@(self.buttonSize.height));
|
||||
}];
|
||||
|
||||
//according to apple super should be called at end of method
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
- (void)didTapButton:(UIButton *)button {
|
||||
// --- Do your changes ie change variables that affect your layout etc ---
|
||||
self.buttonSize = CGSize(200, 200);
|
||||
|
||||
// tell constraints they need updating
|
||||
[self setNeedsUpdateConstraints];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
## Installation
|
||||
Use the [orsome](http://www.youtube.com/watch?v=YaIZF8uUTtk) [CocoaPods](http://github.com/CocoaPods/CocoaPods).
|
||||
|
||||
In your Podfile
|
||||
>`pod 'Masonry'`
|
||||
|
||||
If you want to use masonry without all those pesky 'mas_' prefixes. Add #define MAS_SHORTHAND to your prefix.pch before importing Masonry
|
||||
>`#define MAS_SHORTHAND`
|
||||
|
||||
Get busy Masoning
|
||||
>`#import "Masonry.h"`
|
||||
|
||||
## Code Snippets
|
||||
|
||||
Copy the included code snippets to ``~/Library/Developer/Xcode/UserData/CodeSnippets`` to write your masonry blocks at lightning speed!
|
||||
|
||||
`mas_make` -> ` [<#view#> mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
<#code#>
|
||||
}];`
|
||||
|
||||
`mas_update` -> ` [<#view#> mas_updateConstraints:^(MASConstraintMaker *make) {
|
||||
<#code#>
|
||||
}];`
|
||||
|
||||
`mas_remake` -> ` [<#view#> mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
<#code#>
|
||||
}];`
|
||||
|
||||
## Features
|
||||
* Not limited to subset of Auto Layout. Anything NSLayoutConstraint can do, Masonry can do too!
|
||||
* Great debug support, give your views and constraints meaningful names.
|
||||
* Constraints read like sentences.
|
||||
* No crazy macro magic. Masonry won't pollute the global namespace with macros.
|
||||
* Not string or dictionary based and hence you get compile time checking.
|
||||
|
||||
## TODO
|
||||
* Eye candy
|
||||
* Mac example project
|
||||
* More tests and examples
|
||||
|
||||
32
CustomKeyboard/Model/KBKey.h
Normal file
32
CustomKeyboard/Model/KBKey.h
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// KBKey.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// 简单的键位数据模型,用于描述键盘上的一个键。
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBKeyType) {
|
||||
KBKeyTypeCharacter = 0, // 普通字符输出
|
||||
KBKeyTypeBackspace, // 删除
|
||||
KBKeyTypeShift, // 大小写切换
|
||||
KBKeyTypeModeChange, // 模式切换(如 123/ABC)
|
||||
KBKeyTypeSpace, // 空格
|
||||
KBKeyTypeReturn, // 回车/发送
|
||||
KBKeyTypeGlobe, // 系统地球键
|
||||
KBKeyTypeCustom, // 自定义功能占位
|
||||
KBKeyTypeSymbolsToggle // 数字面板内的“#+=/123”切换
|
||||
};
|
||||
|
||||
@interface KBKey : NSObject
|
||||
|
||||
@property (nonatomic, assign) KBKeyType type;
|
||||
@property (nonatomic, copy) NSString *title; // 显示标题
|
||||
@property (nonatomic, copy) NSString *output; // 字符键插入的文本
|
||||
|
||||
+ (instancetype)keyWithTitle:(NSString *)title output:(NSString *)output;
|
||||
+ (instancetype)keyWithTitle:(NSString *)title type:(KBKeyType)type;
|
||||
|
||||
@end
|
||||
27
CustomKeyboard/Model/KBKey.m
Normal file
27
CustomKeyboard/Model/KBKey.m
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// KBKey.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBKey.h"
|
||||
|
||||
@implementation KBKey
|
||||
|
||||
+ (instancetype)keyWithTitle:(NSString *)title output:(NSString *)output {
|
||||
KBKey *k = [[KBKey alloc] init];
|
||||
k.type = KBKeyTypeCharacter;
|
||||
k.title = title ?: @"";
|
||||
k.output = output ?: title ?: @"";
|
||||
return k;
|
||||
}
|
||||
|
||||
+ (instancetype)keyWithTitle:(NSString *)title type:(KBKeyType)type {
|
||||
KBKey *k = [[KBKey alloc] init];
|
||||
k.type = type;
|
||||
k.title = title ?: @"";
|
||||
k.output = @"";
|
||||
return k;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
57
CustomKeyboard/Network/KBNetworkManager.h
Normal file
57
CustomKeyboard/Network/KBNetworkManager.h
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// KBNetworkManager.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// 轻量网络层封装(扩展安全)。支持 GET/POST(JSON)。
|
||||
// 注意:键盘扩展需要"允许完全访问"后才可联网,
|
||||
// 建议由宿主控制器在确认后调用 `setEnabled:YES` 再发起请求。
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSErrorDomain const KBNetworkErrorDomain;
|
||||
typedef NS_ERROR_ENUM(KBNetworkErrorDomain, KBNetworkError) {
|
||||
KBNetworkErrorDisabled = 1, // 未启用网络(例如未开启完全访问)
|
||||
KBNetworkErrorInvalidURL = 2,
|
||||
KBNetworkErrorInvalidResponse = 3,
|
||||
KBNetworkErrorDecodeFailed = 4,
|
||||
};
|
||||
|
||||
/// 简单的 JSON 回调:json 为 NSDictionary/NSArray 或者在非 JSON 情况下返回 NSData
|
||||
typedef void(^KBNetworkCompletion)(id _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error);
|
||||
|
||||
@interface KBNetworkManager : NSObject
|
||||
|
||||
/// 单例
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 是否允许网络(默认为 NO,宿主在合适时机置 YES)
|
||||
@property (atomic, assign, getter=isEnabled) BOOL enabled;
|
||||
|
||||
/// 可选的基础域名,例如 https://api.example.com
|
||||
@property (nonatomic, strong, nullable) NSURL *baseURL;
|
||||
|
||||
/// 全局默认请求头(每次请求会与局部 headers 合并,局部优先)
|
||||
@property (nonatomic, copy) NSDictionary<NSString *, NSString *> *defaultHeaders;
|
||||
|
||||
/// 超时时间(默认 10s)
|
||||
@property (nonatomic, assign) NSTimeInterval timeout;
|
||||
|
||||
/// GET 请求,parameters 会拼到 URL 上
|
||||
- (nullable NSURLSessionDataTask *)GET:(NSString *)path
|
||||
parameters:(nullable NSDictionary *)parameters
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
completion:(KBNetworkCompletion)completion;
|
||||
|
||||
/// POST JSON 请求,jsonBody 会以 application/json 发送
|
||||
- (nullable NSURLSessionDataTask *)POST:(NSString *)path
|
||||
jsonBody:(nullable id)jsonBody
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
completion:(KBNetworkCompletion)completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
169
CustomKeyboard/Network/KBNetworkManager.m
Normal file
169
CustomKeyboard/Network/KBNetworkManager.m
Normal file
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// KBNetworkManager.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBNetworkManager.h"
|
||||
#import "AFNetworking.h"
|
||||
#import "KBAuthManager.h"
|
||||
|
||||
NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
|
||||
|
||||
@interface KBNetworkManager ()
|
||||
@property (nonatomic, strong) AFHTTPSessionManager *manager; // AFN 管理器(ephemeral 配置)
|
||||
// 私有错误派发
|
||||
- (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion;
|
||||
@end
|
||||
|
||||
@implementation KBNetworkManager
|
||||
|
||||
+ (instancetype)shared {
|
||||
static KBNetworkManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBNetworkManager new]; });
|
||||
return m;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_enabled = NO; // 键盘扩展默认无网络能力,需外部显式开启
|
||||
_timeout = 10.0;
|
||||
_defaultHeaders = @{ @"Accept": @"application/json" };
|
||||
// 设置基础域名,路径可相对该地址拼接
|
||||
_baseURL = [NSURL URLWithString:KB_BASE_URL];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSURLSessionDataTask *)GET:(NSString *)path
|
||||
parameters:(NSDictionary *)parameters
|
||||
headers:(NSDictionary<NSString *,NSString *> *)headers
|
||||
completion:(KBNetworkCompletion)completion {
|
||||
if (![self ensureEnabled:completion]) return nil;
|
||||
NSString *urlString = [self buildURLStringWithPath:path];
|
||||
if (!urlString) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; }
|
||||
// 使用 AFHTTPRequestSerializer 生成带参数与头的请求
|
||||
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
|
||||
serializer.timeoutInterval = self.timeout;
|
||||
NSMutableURLRequest *req = [serializer requestWithMethod:@"GET"
|
||||
URLString:urlString
|
||||
parameters:parameters
|
||||
error:NULL];
|
||||
[self applyHeaders:headers toMutableRequest:req contentType:nil];
|
||||
return [self startAFTaskWithRequest:req completion:completion];
|
||||
}
|
||||
|
||||
- (NSURLSessionDataTask *)POST:(NSString *)path
|
||||
jsonBody:(id)jsonBody
|
||||
headers:(NSDictionary<NSString *,NSString *> *)headers
|
||||
completion:(KBNetworkCompletion)completion {
|
||||
if (![self ensureEnabled:completion]) return nil;
|
||||
NSString *urlString = [self buildURLStringWithPath:path];
|
||||
if (!urlString) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; }
|
||||
// 用 JSON 序列化器生成 JSON Body 的请求
|
||||
AFJSONRequestSerializer *serializer = [AFJSONRequestSerializer serializer];
|
||||
serializer.timeoutInterval = self.timeout;
|
||||
NSError *error = nil;
|
||||
NSMutableURLRequest *req = [serializer requestWithMethod:@"POST"
|
||||
URLString:urlString
|
||||
parameters:jsonBody
|
||||
error:&error];
|
||||
if (error) { if (completion) completion(nil, nil, error); return nil; }
|
||||
[self applyHeaders:headers toMutableRequest:req contentType:nil];
|
||||
return [self startAFTaskWithRequest:req completion:completion];
|
||||
}
|
||||
|
||||
#pragma mark - Core
|
||||
|
||||
- (BOOL)ensureEnabled:(KBNetworkCompletion)completion {
|
||||
if (!self.isEnabled) {
|
||||
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDisabled userInfo:@{NSLocalizedDescriptionKey: @"网络未启用(可能未开启完全访问)"}];
|
||||
if (completion) completion(nil, nil, e);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSString *)buildURLStringWithPath:(NSString *)path {
|
||||
if (path.length == 0) return nil;
|
||||
if ([path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) {
|
||||
return path;
|
||||
}
|
||||
if (self.baseURL) {
|
||||
return [NSURL URLWithString:path relativeToURL:self.baseURL].absoluteURL.absoluteString;
|
||||
}
|
||||
return path; // 当无 baseURL 且 path 不是完整 URL 时,让 AFN 处理(可能失败)
|
||||
}
|
||||
|
||||
- (void)applyHeaders:(NSDictionary<NSString *,NSString *> *)headers toMutableRequest:(NSMutableURLRequest *)req contentType:(NSString *)contentType {
|
||||
// 合并默认头与局部头,并注入授权头(若可用)。局部覆盖优先。
|
||||
NSMutableDictionary *all = [self.defaultHeaders mutableCopy] ?: [NSMutableDictionary new];
|
||||
NSDictionary *auth = [[KBAuthManager shared] authorizationHeader];
|
||||
[auth enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
|
||||
if (contentType) all[@"Content-Type"] = contentType;
|
||||
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
|
||||
[all enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { [req setValue:obj forHTTPHeaderField:key]; }];
|
||||
}
|
||||
|
||||
- (NSURLSessionDataTask *)startAFTaskWithRequest:(NSURLRequest *)req completion:(KBNetworkCompletion)completion {
|
||||
// 响应先用原始数据返回,再按 Content-Type 解析 JSON(与原实现一致)
|
||||
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
|
||||
NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
|
||||
if (error) { if (completion) completion(nil, response, error); return; }
|
||||
NSData *data = (NSData *)responseObject;
|
||||
if (![data isKindOfClass:[NSData class]]) {
|
||||
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:@"无数据"}]);
|
||||
return;
|
||||
}
|
||||
NSString *ct = nil;
|
||||
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"];
|
||||
}
|
||||
BOOL looksJSON = (ct && [ct.lowercaseString containsString:@"application/json"]);
|
||||
if (looksJSON) {
|
||||
NSError *jsonErr = nil;
|
||||
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr];
|
||||
if (jsonErr) { if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDecodeFailed userInfo:@{NSLocalizedDescriptionKey:@"JSON解析失败"}]); return; }
|
||||
if (completion) completion(json, response, nil);
|
||||
} else {
|
||||
if (completion) completion(data, response, nil);
|
||||
}
|
||||
}];
|
||||
[task resume];
|
||||
return task;
|
||||
}
|
||||
|
||||
#pragma mark - AFHTTPSessionManager
|
||||
|
||||
- (AFHTTPSessionManager *)manager {
|
||||
if (!_manager) {
|
||||
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||
cfg.timeoutIntervalForRequest = self.timeout;
|
||||
cfg.timeoutIntervalForResource = MAX(self.timeout, 30.0);
|
||||
if (@available(iOS 11.0, *)) { cfg.waitsForConnectivity = YES; }
|
||||
_manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:cfg];
|
||||
// 默认不使用 JSON 解析器,保持原生数据,再按需解析
|
||||
_manager.responseSerializer = [AFHTTPResponseSerializer serializer];
|
||||
}
|
||||
return _manager;
|
||||
}
|
||||
|
||||
#pragma mark - Private helpers
|
||||
|
||||
- (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion {
|
||||
NSString *msg = @"网络错误";
|
||||
switch (code) {
|
||||
case KBNetworkErrorDisabled: msg = @"网络未启用(可能未开启完全访问)"; break;
|
||||
case KBNetworkErrorInvalidURL: msg = @"无效的URL"; break;
|
||||
case KBNetworkErrorInvalidResponse: msg = @"无效的响应"; break;
|
||||
case KBNetworkErrorDecodeFailed: msg = @"解析失败"; break;
|
||||
default: break;
|
||||
}
|
||||
NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain
|
||||
code:code
|
||||
userInfo:@{NSLocalizedDescriptionKey: msg}];
|
||||
if (completion) completion(nil, nil, e);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,10 +12,26 @@
|
||||
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.
|
||||
|
||||
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
|
||||
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.width
|
||||
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
|
||||
#define imageNamed(s) [UIImage imageNamed:s]
|
||||
|
||||
// 公共配置
|
||||
#import "KBConfig.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBHUD.h" // 复用 App 内的 HUD 封装
|
||||
#import "KBLocalizationManager.h" // 复用多语言封装(可在扩展内使用)
|
||||
|
||||
// 通用链接(Universal Links)统一配置
|
||||
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
|
||||
#define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
|
||||
// 在扩展内,启用 URL Bridge(仅在显式的用户点击动作中使用)
|
||||
// 这样即便宿主 App(如备忘录)拒绝 extensionContext 的 openURL,仍可通过响应链兜底拉起容器 App。
|
||||
#ifndef KB_URL_BRIDGE_ENABLE
|
||||
#define KB_URL_BRIDGE_ENABLE 1
|
||||
#endif
|
||||
|
||||
|
||||
#endif /* PrefixHeader_pch */
|
||||
|
||||
30
CustomKeyboard/Utils/KBURLOpenBridge.h
Normal file
30
CustomKeyboard/Utils/KBURLOpenBridge.h
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// KBURLOpenBridge.h
|
||||
// 非公开:通过响应链查找 `openURL:` 选择器,尝试在扩展环境中打开自定义 scheme。
|
||||
// 警告:存在审核风险。默认仅 Debug 启用(见 KB_URL_BRIDGE_ENABLE)。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#ifndef KB_URL_BRIDGE_ENABLE
|
||||
#if DEBUG
|
||||
#define KB_URL_BRIDGE_ENABLE 1
|
||||
#else
|
||||
#define KB_URL_BRIDGE_ENABLE 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@interface KBURLOpenBridge : NSObject
|
||||
|
||||
/// 尝试通过响应链调用 openURL:(仅在 KB_URL_BRIDGE_ENABLE 为 1 时执行)。
|
||||
/// @param url 自定义 scheme,如 kbkeyboard://settings
|
||||
/// @param start 起始 responder(传 self 或任意视图)
|
||||
/// @return 是否看起来已发起打开动作(不保证一定成功)
|
||||
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
46
CustomKeyboard/Utils/KBURLOpenBridge.m
Normal file
46
CustomKeyboard/Utils/KBURLOpenBridge.m
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// KBURLOpenBridge.m
|
||||
//
|
||||
|
||||
#import "KBURLOpenBridge.h"
|
||||
#import <objc/message.h>
|
||||
|
||||
@implementation KBURLOpenBridge
|
||||
|
||||
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start {
|
||||
#if KB_URL_BRIDGE_ENABLE
|
||||
if (!url || !start) return NO;
|
||||
SEL sel = NSSelectorFromString(@"openURL:");
|
||||
UIResponder *responder = start;
|
||||
while (responder) {
|
||||
@try {
|
||||
if ([responder respondsToSelector:sel]) {
|
||||
// 尽量按签名调用;若失败则回退 performSelector
|
||||
BOOL handled = NO;
|
||||
// 尝试 (BOOL)openURL:(NSURL *)
|
||||
BOOL (*funcBool)(id, SEL, NSURL *) = (BOOL (*)(id, SEL, NSURL *))objc_msgSend;
|
||||
if (funcBool) {
|
||||
handled = funcBool(responder, sel, url);
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[responder performSelector:sel withObject:url];
|
||||
handled = YES;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
} @catch (__unused NSException *e) {
|
||||
// ignore and continue
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return NO;
|
||||
#else
|
||||
(void)url; (void)start;
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
19
CustomKeyboard/View/KBFullAccessGuideView.h
Normal file
19
CustomKeyboard/View/KBFullAccessGuideView.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// KBFullAccessGuideView.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// A lightweight overlay prompting user to enable "Allow Full Access".
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface KBFullAccessGuideView : UIView
|
||||
|
||||
/// Present the guide overlay inside a given parent view (or its window).
|
||||
+ (void)showInView:(UIView *)parent;
|
||||
|
||||
/// Dismiss if shown.
|
||||
+ (void)dismissFromView:(UIView *)parent;
|
||||
|
||||
@end
|
||||
|
||||
229
CustomKeyboard/View/KBFullAccessGuideView.m
Normal file
229
CustomKeyboard/View/KBFullAccessGuideView.m
Normal file
@@ -0,0 +1,229 @@
|
||||
//
|
||||
// KBFullAccessGuideView.m
|
||||
// CustomKeyboard
|
||||
// 没有开启完全访问的提示框
|
||||
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||
#import "KBHUD.h"
|
||||
#import "KBURLOpenBridge.h"
|
||||
|
||||
@interface KBFullAccessGuideView ()
|
||||
@property (nonatomic, strong) UIControl *backdrop;
|
||||
@property (nonatomic, strong) UIView *card;
|
||||
// 预先保存当前键盘控制器,避免运行时通过响应链找不到
|
||||
@property (nonatomic, weak) UIInputViewController *ivc;
|
||||
@end
|
||||
|
||||
@implementation KBFullAccessGuideView
|
||||
|
||||
+ (instancetype)build {
|
||||
KBFullAccessGuideView *v = [[KBFullAccessGuideView alloc] initWithFrame:CGRectZero];
|
||||
[v setupUI];
|
||||
return v;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
self.backdrop = [[UIControl alloc] init];
|
||||
self.backdrop.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.35];
|
||||
// [self.backdrop addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self addSubview:self.backdrop];
|
||||
[self.backdrop mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
|
||||
self.card = [[UIView alloc] init];
|
||||
self.card.backgroundColor = [UIColor whiteColor];
|
||||
self.card.layer.cornerRadius = 16;
|
||||
self.card.layer.masksToBounds = YES;
|
||||
[self addSubview:self.card];
|
||||
[self.card mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.equalTo(self);
|
||||
make.left.equalTo(self).offset(28);
|
||||
make.right.equalTo(self).offset(-28);
|
||||
}];
|
||||
|
||||
UILabel *title = [UILabel new];
|
||||
title.text = @"开启【允许完全访问】,体验完整功能";
|
||||
title.font = [UIFont boldSystemFontOfSize:16];
|
||||
title.textColor = [UIColor blackColor];
|
||||
title.textAlignment = NSTextAlignmentCenter;
|
||||
[self.card addSubview:title];
|
||||
[title mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.card).offset(16);
|
||||
make.left.right.equalTo(self.card).insets(UIEdgeInsetsMake(0, 16, 0, 16));
|
||||
}];
|
||||
|
||||
// 模拟两行开关(纯展示,不真实控制)
|
||||
UIView *box = [UIView new];
|
||||
box.backgroundColor = [UIColor colorWithWhite:0.98 alpha:1.0];
|
||||
box.layer.cornerRadius = 12;
|
||||
[self.card addSubview:box];
|
||||
[box mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(title.mas_bottom).offset(12);
|
||||
make.left.equalTo(self.card).offset(16);
|
||||
make.right.equalTo(self.card).offset(-16);
|
||||
make.height.mas_equalTo(100);
|
||||
}];
|
||||
|
||||
UILabel *row1 = [UILabel new]; row1.text = @"恋爱键盘"; row1.textColor = [UIColor blackColor];
|
||||
UILabel *row2 = [UILabel new]; row2.text = @"允许完全访问"; row2.textColor = [UIColor blackColor];
|
||||
[box addSubview:row1]; [box addSubview:row2];
|
||||
[row1 mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(box).offset(16); make.top.equalTo(box).offset(14); }];
|
||||
UIView *line = [UIView new]; line.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
|
||||
[box addSubview:line];
|
||||
[line mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(box).offset(12); make.right.equalTo(box).offset(-12); make.centerY.equalTo(box); make.height.mas_equalTo(1); }];
|
||||
[row2 mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(box).offset(16); make.bottom.equalTo(box).offset(-14); }];
|
||||
|
||||
// 右侧绿色开关的装饰
|
||||
UIView* (^switchView)(void) = ^UIView *{
|
||||
UIView *sw = [UIView new];
|
||||
sw.backgroundColor = [UIColor colorWithRed:0.12 green:0.75 blue:0.35 alpha:1.0];
|
||||
sw.layer.cornerRadius = 15;
|
||||
UIView *dot = [UIView new];
|
||||
dot.backgroundColor = [UIColor whiteColor];
|
||||
dot.layer.cornerRadius = 12;
|
||||
[sw addSubview:dot];
|
||||
[dot mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(sw); make.right.equalTo(sw).offset(-3); make.width.height.mas_equalTo(24); }];
|
||||
[sw mas_makeConstraints:^(MASConstraintMaker *make) { make.width.mas_equalTo(52); make.height.mas_equalTo(30); }];
|
||||
return sw;
|
||||
};
|
||||
UIView *sw1 = switchView(); UIView *sw2 = switchView();
|
||||
[box addSubview:sw1]; [box addSubview:sw2];
|
||||
[sw1 mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(box).offset(-12); make.centerY.equalTo(row1); }];
|
||||
[sw2 mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(box).offset(-12); make.centerY.equalTo(row2); }];
|
||||
|
||||
UIButton *go = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
go.backgroundColor = [UIColor blackColor];
|
||||
[go setTitle:@"去开启" forState:UIControlStateNormal];
|
||||
[go setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
go.titleLabel.font = [UIFont boldSystemFontOfSize:18];
|
||||
go.layer.cornerRadius = 12;
|
||||
[go addTarget:self action:@selector(onTapGoEnable) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.card addSubview:go];
|
||||
[go mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(box.mas_bottom).offset(16);
|
||||
make.left.equalTo(self.card).offset(16);
|
||||
make.right.equalTo(self.card).offset(-16);
|
||||
make.height.mas_equalTo(48);
|
||||
make.bottom.equalTo(self.card).offset(-16);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)presentIn:(UIView *)parent {
|
||||
if (!parent) return;
|
||||
UIView *container = parent; // 关键:加到键盘视图树中,而不是 window
|
||||
self.frame = container.bounds;
|
||||
self.alpha = 0;
|
||||
[container addSubview:self];
|
||||
[self mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(container); }];
|
||||
[UIView animateWithDuration:0.2 animations:^{ self.alpha = 1; }];
|
||||
}
|
||||
|
||||
- (void)dismiss {
|
||||
[UIView animateWithDuration:0.18 animations:^{ self.alpha = 0; } completion:^(BOOL finished) {
|
||||
[self removeFromSuperview];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)showInView:(UIView *)parent {
|
||||
if (!parent) return;
|
||||
// 避免重复(仅在 parent 层级检查)
|
||||
for (UIView *v in parent.subviews) {
|
||||
if ([v isKindOfClass:[KBFullAccessGuideView class]]) return;
|
||||
}
|
||||
KBFullAccessGuideView *view = [KBFullAccessGuideView build];
|
||||
// 预取 ivc
|
||||
view.ivc = KBFindInputViewController(parent);
|
||||
[view presentIn:parent];
|
||||
}
|
||||
|
||||
+ (void)dismissFromView:(UIView *)parent {
|
||||
UIView *container = parent;
|
||||
if (!container) return;
|
||||
for (UIView *v in container.subviews) {
|
||||
if ([v isKindOfClass:[KBFullAccessGuideView class]]) {
|
||||
[(KBFullAccessGuideView *)v dismiss];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
// 工具方法已提取到 KBResponderUtils.h
|
||||
// 打开主 App,引导用户去系统设置开启完全访问:优先 Scheme,失败再试 UL;仍失败则提示手动路径。
|
||||
- (void)onTapGoEnable {
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) { [self dismiss]; return; }
|
||||
|
||||
// 自定义 Scheme(App 里在 openURL 中转到设置页)
|
||||
// 统一使用主 App 的自定义 Scheme
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//settings?src=kb_extension", KB_APP_SCHEME]];
|
||||
// Universal Link(需 AASA/Associated Domains 配置且 KB_UL_BASE 与域名一致)
|
||||
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]];
|
||||
|
||||
void (^finish)(BOOL) = ^(BOOL ok){
|
||||
if (ok) { [self dismiss]; }
|
||||
else {
|
||||
[KBHUD showInfo:@"无法自动打开,请按路径:设置→通用→键盘→键盘→恋爱键盘→允许完全访问"]; // 失败兜底提示
|
||||
}
|
||||
};
|
||||
|
||||
// 先试 Scheme(更可能被宿主允许直接拉起 App)
|
||||
if (scheme) {
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok) {
|
||||
if (ok) { finish(YES); return; }
|
||||
if (ul) {
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok2) {
|
||||
if (ok2) { finish(YES); return; }
|
||||
// 兜底:在用户点击触发的场景下,尝试通过响应链调用 openURL:
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
|
||||
if (!bridged && ul) {
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
finish(bridged);
|
||||
}];
|
||||
} else {
|
||||
// 没有 UL,则直接尝试桥接 Scheme
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
finish(bridged);
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
// 无 scheme 时,直接尝试 UL
|
||||
if (ul) {
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
|
||||
if (ok) { finish(YES); return; }
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
finish(bridged);
|
||||
}];
|
||||
} else {
|
||||
finish(NO);
|
||||
}
|
||||
}
|
||||
@end
|
||||
39
CustomKeyboard/View/KBFunctionBarView.h
Normal file
39
CustomKeyboard/View/KBFunctionBarView.h
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// KBFunctionBarView.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 功能区顶部的Bar:左侧4个按钮,右侧3个按钮
|
||||
@class KBFunctionBarView;
|
||||
|
||||
@protocol KBFunctionBarViewDelegate <NSObject>
|
||||
@optional
|
||||
/// 左侧 4 个按钮点击(index: 0~3)
|
||||
- (void)functionBarView:(KBFunctionBarView *)bar didTapLeftAtIndex:(NSInteger)index;
|
||||
/// 右侧 3 个按钮点击(index: 0~2)
|
||||
- (void)functionBarView:(KBFunctionBarView *)bar didTapRightAtIndex:(NSInteger)index;
|
||||
@end
|
||||
|
||||
@interface KBFunctionBarView : UIView
|
||||
|
||||
@property (nonatomic, weak, nullable) id<KBFunctionBarViewDelegate> delegate;
|
||||
|
||||
/// 左侧4个按钮(懒加载创建,等宽水平排布)
|
||||
@property (nonatomic, strong, readonly) NSArray<UIButton *> *leftButtons;
|
||||
|
||||
/// 右侧3个按钮(懒加载创建,等宽水平排布,靠右)
|
||||
@property (nonatomic, strong, readonly) NSArray<UIButton *> *rightButtons;
|
||||
|
||||
/// 配置按钮标题(可选)
|
||||
@property (nonatomic, copy) NSArray<NSString *> *leftTitles; // 默认 @[@"帮回", @"会说", @"话术", @"更多"]
|
||||
@property (nonatomic, copy) NSArray<NSString *> *rightTitles; // 默认 @[@"❤", @"收藏", @"宫格"]
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
226
CustomKeyboard/View/KBFunctionBarView.m
Normal file
226
CustomKeyboard/View/KBFunctionBarView.m
Normal file
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// KBFunctionBarView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
// 功能 - barview
|
||||
|
||||
#import "KBFunctionBarView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBResponderUtils.h" // 查找 UIInputViewController,用于系统切换输入法
|
||||
|
||||
@interface KBFunctionBarView ()
|
||||
@property (nonatomic, strong) UIView *leftContainer; // 左侧按钮容器
|
||||
@property (nonatomic, strong) UIView *rightContainer; // 右侧按钮容器
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *rightButtonsInternal;
|
||||
@property (nonatomic, strong) UIButton *globeButtonInternal; // 可选:系统“切换输入法”键
|
||||
@end
|
||||
|
||||
@implementation KBFunctionBarView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
_leftTitles = @[@"ABC"];
|
||||
_rightTitles = @[@"Upgrade VIP"];
|
||||
[self buildUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSArray<UIButton *> *)leftButtons { return self.leftButtonsInternal; }
|
||||
- (NSArray<UIButton *> *)rightButtons { return self.rightButtonsInternal; }
|
||||
|
||||
|
||||
#pragma mark - UI
|
||||
|
||||
- (void)buildUI {
|
||||
// 左右两个容器,方便分别布局
|
||||
[self addSubview:self.leftContainer];
|
||||
[self addSubview:self.globeButtonInternal];
|
||||
[self addSubview:self.rightContainer];
|
||||
|
||||
[self.rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.mas_right).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(36);
|
||||
}];
|
||||
|
||||
// 左侧地球键(按需显示)
|
||||
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
make.right.equalTo(self.rightContainer.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(36);
|
||||
}];
|
||||
|
||||
// 左侧4个等宽按钮
|
||||
NSMutableArray<UIButton *> *leftBtns = [NSMutableArray arrayWithCapacity:4];
|
||||
UIView *prev = nil;
|
||||
for (NSInteger i = 0; i < self.leftTitles.count; i++) {
|
||||
UIButton *btn = [self buildButtonWithTitle:(i < self.leftTitles.count ? self.leftTitles[i] : [NSString stringWithFormat:@"L%ld", (long)i])];
|
||||
btn.tag = 100 + i;
|
||||
[btn addTarget:self action:@selector(onLeftTap:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.leftContainer addSubview:btn];
|
||||
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
if (prev) {
|
||||
make.left.equalTo(prev.mas_right).offset(8);
|
||||
make.width.equalTo(prev);
|
||||
} else {
|
||||
make.left.equalTo(self.leftContainer.mas_left);
|
||||
}
|
||||
make.top.bottom.equalTo(self.leftContainer);
|
||||
}];
|
||||
prev = btn;
|
||||
[leftBtns addObject:btn];
|
||||
}
|
||||
[prev mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.leftContainer.mas_right);
|
||||
}];
|
||||
self.leftButtonsInternal = leftBtns.copy;
|
||||
|
||||
// 右侧N个按钮(靠右、两两等宽)
|
||||
NSMutableArray<UIButton *> *rightBtns = [NSMutableArray arrayWithCapacity:3];
|
||||
for (NSInteger i = 0; i < self.rightTitles.count; i++) {
|
||||
UIButton *btn = [self buildButtonWithTitle:(i < self.rightTitles.count ? self.rightTitles[i] : [NSString stringWithFormat:@"R%ld", (long)i])];
|
||||
btn.tag = 200 + i;
|
||||
[self.rightContainer addSubview:btn];
|
||||
[btn addTarget:self action:@selector(onRightTap:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[rightBtns addObject:btn];
|
||||
}
|
||||
|
||||
// 从右往左链式布局,保证整体靠右;支持 1/2/3... 任意数量
|
||||
UIView *prevRight = nil; // 指向右侧已布局的按钮
|
||||
for (NSInteger i = rightBtns.count - 1; i >= 0; i--) {
|
||||
UIButton *btn = rightBtns[i];
|
||||
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
if (!prevRight) {
|
||||
// 最右侧按钮贴右
|
||||
make.right.equalTo(self.rightContainer.mas_right);
|
||||
} else {
|
||||
// 其余按钮紧挨左侧兄弟,且与其等宽
|
||||
make.right.equalTo(prevRight.mas_left).offset(-8);
|
||||
make.width.equalTo(prevRight);
|
||||
}
|
||||
make.top.bottom.equalTo(self.rightContainer);
|
||||
}];
|
||||
prevRight = btn;
|
||||
}
|
||||
// 最左侧一个不超出容器左边(允许根据内容自然宽度收缩)
|
||||
if (prevRight) {
|
||||
[prevRight mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.greaterThanOrEqualTo(self.rightContainer.mas_left);
|
||||
}];
|
||||
}
|
||||
|
||||
self.rightButtonsInternal = rightBtns.copy;
|
||||
|
||||
// 初始刷新地球键可见性与事件绑定
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onLeftTap:(UIButton *)sender {
|
||||
NSInteger idx = sender.tag - 100;
|
||||
if ([self.delegate respondsToSelector:@selector(functionBarView:didTapLeftAtIndex:)]) {
|
||||
[self.delegate functionBarView:self didTapLeftAtIndex:idx];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onRightTap:(UIButton *)sender {
|
||||
NSInteger idx = sender.tag - 200;
|
||||
if ([self.delegate respondsToSelector:@selector(functionBarView:didTapRightAtIndex:)]) {
|
||||
[self.delegate functionBarView:self didTapRightAtIndex:idx];
|
||||
}
|
||||
}
|
||||
|
||||
- (UIButton *)buildButtonWithTitle:(NSString *)title {
|
||||
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
btn.layer.cornerRadius = 18;
|
||||
btn.layer.masksToBounds = YES;
|
||||
btn.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
btn.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||
[btn setTitle:title forState:UIControlStateNormal];
|
||||
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
return btn;
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIView *)leftContainer {
|
||||
if (!_leftContainer) {
|
||||
_leftContainer = [[UIView alloc] init];
|
||||
}
|
||||
return _leftContainer;
|
||||
}
|
||||
|
||||
- (UIView *)rightContainer {
|
||||
if (!_rightContainer) {
|
||||
_rightContainer = [[UIView alloc] init];
|
||||
}
|
||||
return _rightContainer;
|
||||
}
|
||||
|
||||
- (UIButton *)globeButtonInternal {
|
||||
if (!_globeButtonInternal) {
|
||||
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_globeButtonInternal.layer.cornerRadius = 16;
|
||||
_globeButtonInternal.layer.masksToBounds = YES;
|
||||
_globeButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
[_globeButtonInternal setTitle:@"🌐" forState:UIControlStateNormal];
|
||||
[_globeButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
}
|
||||
return _globeButtonInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Globe (Input Mode Switch)
|
||||
|
||||
- (void)kb_refreshGlobeVisibility {
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
BOOL needSwitchKey = YES;
|
||||
if (ivc && [ivc respondsToSelector:@selector(needsInputModeSwitchKey)]) {
|
||||
needSwitchKey = ivc.needsInputModeSwitchKey;
|
||||
}
|
||||
|
||||
self.globeButtonInternal.hidden = !needSwitchKey;
|
||||
|
||||
// 左容器左约束:根据是否显示地球键动态调整
|
||||
[self.leftContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
if (needSwitchKey) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
} else {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
}
|
||||
make.right.equalTo(self.rightContainer.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(36);
|
||||
}];
|
||||
|
||||
// 绑定系统输入法切换事件
|
||||
[self.globeButtonInternal removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
|
||||
if (needSwitchKey && ivc) {
|
||||
SEL sel = NSSelectorFromString(@"handleInputModeListFromView:withEvent:");
|
||||
if ([ivc respondsToSelector:sel]) {
|
||||
[self.globeButtonInternal addTarget:ivc action:sel forControlEvents:UIControlEventAllTouchEvents];
|
||||
} else {
|
||||
[self.globeButtonInternal addTarget:ivc action:@selector(advanceToNextInputMode) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
@end
|
||||
23
CustomKeyboard/View/KBFunctionPasteView.h
Normal file
23
CustomKeyboard/View/KBFunctionPasteView.h
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// KBFunctionPasteView.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 粘贴提示输入框区域(左侧图标+占位文案,圆角白底)
|
||||
@interface KBFunctionPasteView : UIView
|
||||
|
||||
/// 左侧图标
|
||||
@property (nonatomic, strong, readonly) UIImageView *iconView;
|
||||
|
||||
/// 提示文案,例如:点击粘贴TA的话
|
||||
@property (nonatomic, strong, readonly) UILabel *placeholderLabel;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
75
CustomKeyboard/View/KBFunctionPasteView.m
Normal file
75
CustomKeyboard/View/KBFunctionPasteView.m
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// KBFunctionPasteView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
// 粘贴View
|
||||
|
||||
#import "KBFunctionPasteView.h"
|
||||
#import "Masonry.h"
|
||||
|
||||
@interface KBFunctionPasteView ()
|
||||
@property (nonatomic, strong) UIImageView *iconViewInternal;
|
||||
@property (nonatomic, strong) UILabel *placeholderLabelInternal;
|
||||
@end
|
||||
|
||||
@implementation KBFunctionPasteView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
// 白底圆角容器
|
||||
self.backgroundColor = [UIColor colorWithWhite:1 alpha:0.95];
|
||||
self.layer.cornerRadius = 12.0;
|
||||
self.layer.masksToBounds = YES;
|
||||
|
||||
[self addSubview:self.iconViewInternal];
|
||||
[self addSubview:self.placeholderLabelInternal];
|
||||
|
||||
[self.iconViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.width.height.mas_equalTo(20);
|
||||
}];
|
||||
[self.placeholderLabelInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.iconViewInternal.mas_right).offset(8);
|
||||
make.right.equalTo(self.mas_right).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
}];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIImageView *)iconViewInternal {
|
||||
if (!_iconViewInternal) {
|
||||
_iconViewInternal = [[UIImageView alloc] init];
|
||||
// 用简单的系统表情代替资源图(项目可替换成实际图片)
|
||||
UILabel *emoji = [[UILabel alloc] init];
|
||||
emoji.text = @"📋"; // 粘贴/剪贴板含义
|
||||
emoji.font = [UIFont systemFontOfSize:18];
|
||||
emoji.textAlignment = NSTextAlignmentCenter;
|
||||
[_iconViewInternal addSubview:emoji];
|
||||
[emoji mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(_iconViewInternal);
|
||||
}];
|
||||
}
|
||||
return _iconViewInternal;
|
||||
}
|
||||
|
||||
- (UILabel *)placeholderLabelInternal {
|
||||
if (!_placeholderLabelInternal) {
|
||||
_placeholderLabelInternal = [[UILabel alloc] init];
|
||||
_placeholderLabelInternal.text = @"点击粘贴TA的话";
|
||||
_placeholderLabelInternal.textColor = [UIColor colorWithRed:0.20 green:0.64 blue:0.54 alpha:1.0];
|
||||
_placeholderLabelInternal.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
}
|
||||
return _placeholderLabelInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Expose
|
||||
|
||||
- (UIImageView *)iconView { return self.iconViewInternal; }
|
||||
- (UILabel *)placeholderLabel { return self.placeholderLabelInternal; }
|
||||
|
||||
@end
|
||||
23
CustomKeyboard/View/KBFunctionTagCell.h
Normal file
23
CustomKeyboard/View/KBFunctionTagCell.h
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// KBFunctionTagCell.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Codex on 2025/10/28.
|
||||
// 话术标签Cell:左图标+右标题,圆角灰白底
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBFunctionTagCell : UICollectionViewCell
|
||||
|
||||
/// 标题
|
||||
@property (nonatomic, strong, readonly) UILabel *titleLabel;
|
||||
|
||||
/// 头像/图标
|
||||
@property (nonatomic, strong, readonly) UIImageView *iconView;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
73
CustomKeyboard/View/KBFunctionTagCell.m
Normal file
73
CustomKeyboard/View/KBFunctionTagCell.m
Normal file
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// KBFunctionTagCell.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Codex on 2025/10/28.
|
||||
//
|
||||
|
||||
#import "KBFunctionTagCell.h"
|
||||
#import "Masonry.h"
|
||||
|
||||
@interface KBFunctionTagCell ()
|
||||
@property (nonatomic, strong) UILabel *titleLabelInternal;
|
||||
@property (nonatomic, strong) UIImageView *iconViewInternal;
|
||||
@end
|
||||
|
||||
@implementation KBFunctionTagCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.contentView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
self.contentView.layer.cornerRadius = 12;
|
||||
self.contentView.layer.masksToBounds = YES;
|
||||
|
||||
[self.contentView addSubview:self.iconViewInternal];
|
||||
[self.contentView addSubview:self.titleLabelInternal];
|
||||
|
||||
[self.iconViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.contentView.mas_left).offset(10);
|
||||
make.centerY.equalTo(self.contentView.mas_centerY);
|
||||
make.width.height.mas_equalTo(24);
|
||||
}];
|
||||
[self.titleLabelInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.iconViewInternal.mas_right).offset(6);
|
||||
make.right.equalTo(self.contentView.mas_right).offset(-10);
|
||||
make.centerY.equalTo(self.contentView.mas_centerY);
|
||||
}];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIImageView *)iconViewInternal {
|
||||
if (!_iconViewInternal) {
|
||||
_iconViewInternal = [[UIImageView alloc] init];
|
||||
UILabel *emoji = [[UILabel alloc] init];
|
||||
emoji.text = @"🙂"; // 占位图标
|
||||
emoji.textAlignment = NSTextAlignmentCenter;
|
||||
emoji.font = [UIFont systemFontOfSize:20];
|
||||
[_iconViewInternal addSubview:emoji];
|
||||
[emoji mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(_iconViewInternal);
|
||||
}];
|
||||
}
|
||||
return _iconViewInternal;
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabelInternal {
|
||||
if (!_titleLabelInternal) {
|
||||
_titleLabelInternal = [[UILabel alloc] init];
|
||||
_titleLabelInternal.font = [UIFont systemFontOfSize:15 weight:UIFontWeightSemibold];
|
||||
_titleLabelInternal.textColor = [UIColor blackColor];
|
||||
}
|
||||
return _titleLabelInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Expose
|
||||
|
||||
- (UILabel *)titleLabel { return self.titleLabelInternal; }
|
||||
- (UIImageView *)iconView { return self.iconViewInternal; }
|
||||
|
||||
@end
|
||||
|
||||
41
CustomKeyboard/View/KBFunctionView.h
Normal file
41
CustomKeyboard/View/KBFunctionView.h
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// KBFunctionView.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@class KBFunctionBarView, KBFunctionPasteView,KBFunctionView;
|
||||
|
||||
@protocol KBFunctionViewDelegate <NSObject>
|
||||
@optional
|
||||
- (void)functionView:(KBFunctionView *_Nullable)functionView didTapToolActionAtIndex:(NSInteger)index;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 整个功能面板视图:顶部Bar + 粘贴区 + 标签列表 + 右侧操作按钮
|
||||
@interface KBFunctionView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<KBFunctionViewDelegate> delegate;
|
||||
|
||||
|
||||
@property (nonatomic, strong, readonly) UICollectionView *collectionView; // 话术分类/标签列表
|
||||
@property (nonatomic, strong, readonly) NSArray<NSString *> *items; // 简单数据源(演示用)
|
||||
|
||||
// 子视图暴露,便于外部接入事件
|
||||
@property (nonatomic, strong, readonly) KBFunctionBarView *barView;
|
||||
@property (nonatomic, strong, readonly) KBFunctionPasteView *pasteView;
|
||||
|
||||
@property (nonatomic, strong, readonly) UIButton *pasteButton; // 右侧-粘贴
|
||||
@property (nonatomic, strong, readonly) UIButton *deleteButton; // 右侧-删除
|
||||
@property (nonatomic, strong, readonly) UIButton *clearButton; // 右侧-清空
|
||||
@property (nonatomic, strong, readonly) UIButton *sendButton; // 右侧-发送
|
||||
|
||||
/// 应用当前皮肤(更新背景/强调色)
|
||||
- (void)kb_applyTheme;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
462
CustomKeyboard/View/KBFunctionView.m
Normal file
462
CustomKeyboard/View/KBFunctionView.m
Normal file
@@ -0,0 +1,462 @@
|
||||
//
|
||||
// KBFunctionView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||
#import "KBFunctionBarView.h"
|
||||
#import "KBFunctionPasteView.h"
|
||||
#import "KBFunctionTagCell.h"
|
||||
#import "Masonry.h"
|
||||
#import <MBProgressHUD.h>
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "KBFullAccessManager.h"
|
||||
#import "KBSkinManager.h"
|
||||
#import "KBURLOpenBridge.h" // 兜底从扩展侧直接尝试 openURL:
|
||||
|
||||
static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
@interface KBFunctionView () <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, KBFunctionBarViewDelegate>
|
||||
// UI
|
||||
@property (nonatomic, strong) KBFunctionBarView *barViewInternal;
|
||||
@property (nonatomic, strong) KBFunctionPasteView *pasteViewInternal;
|
||||
@property (nonatomic, strong) UICollectionView *collectionViewInternal;
|
||||
@property (nonatomic, strong) UIView *rightButtonContainer; // 右侧竖排按钮容器
|
||||
@property (nonatomic, strong) UIButton *pasteButtonInternal;
|
||||
@property (nonatomic, strong) UIButton *deleteButtonInternal;
|
||||
@property (nonatomic, strong) UIButton *clearButtonInternal;
|
||||
@property (nonatomic, strong) UIButton *sendButtonInternal;
|
||||
|
||||
// Data
|
||||
@property (nonatomic, strong) NSArray<NSString *> *itemsInternal;
|
||||
|
||||
// 剪贴板自动检测
|
||||
@property (nonatomic, strong) NSTimer *pasteboardTimer; // 轮询定时器(轻量、主线程)
|
||||
@property (nonatomic, assign) NSInteger lastHandledPBCount; // 上次处理过的 changeCount,避免重复弹窗
|
||||
@end
|
||||
|
||||
@implementation KBFunctionView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
// 背景使用当前主题强调色
|
||||
[self kb_applyTheme];
|
||||
|
||||
[self setupUI];
|
||||
[self reloadDemoData];
|
||||
|
||||
// 初始化剪贴板监控状态
|
||||
_lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount;
|
||||
|
||||
// 监听“完全访问”状态变化,动态启停剪贴板监控,避免在未开完全访问时触发 TCC/XPC 错误日志
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)kb_applyTheme {
|
||||
KBSkinManager *mgr = [KBSkinManager shared];
|
||||
UIColor *accent = mgr.current.accentColor ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
|
||||
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
|
||||
self.backgroundColor = hasImg ? [accent colorWithAlphaComponent:0.65] : accent;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stopPasteboardMonitor];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
|
||||
- (void)setupUI {
|
||||
// 1. 顶部 Bar
|
||||
[self addSubview:self.barViewInternal];
|
||||
[self.barViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self);
|
||||
make.top.equalTo(self.mas_top).offset(6);
|
||||
make.height.mas_equalTo(48);
|
||||
}];
|
||||
|
||||
// 右侧竖排按钮容器
|
||||
[self addSubview:self.rightButtonContainer];
|
||||
[self.rightButtonContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.mas_right).offset(-12);
|
||||
make.top.equalTo(self.barViewInternal.mas_bottom).offset(8);
|
||||
make.bottom.equalTo(self.mas_bottom).offset(-10);
|
||||
make.width.mas_equalTo(72);
|
||||
}];
|
||||
|
||||
// 右侧四个按钮
|
||||
[self.rightButtonContainer addSubview:self.pasteButtonInternal];
|
||||
[self.rightButtonContainer addSubview:self.deleteButtonInternal];
|
||||
[self.rightButtonContainer addSubview:self.clearButtonInternal];
|
||||
[self.rightButtonContainer addSubview:self.sendButtonInternal];
|
||||
|
||||
// 竖向排布:粘贴、删除、清空为等高;发送优先更高,但允许在空间不足时压缩
|
||||
CGFloat smallH = 44;
|
||||
CGFloat bigH = 56;
|
||||
// 原 10 在键盘总高度 276 下容易超出容器,改为 8 以避免 AutoLayout 冲突
|
||||
CGFloat vSpace = 8;
|
||||
[self.pasteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.rightButtonContainer.mas_top);
|
||||
make.left.right.equalTo(self.rightButtonContainer);
|
||||
make.height.mas_equalTo(smallH);
|
||||
}];
|
||||
[self.deleteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.pasteButtonInternal.mas_bottom).offset(vSpace);
|
||||
make.left.right.equalTo(self.rightButtonContainer);
|
||||
make.height.equalTo(self.pasteButtonInternal);
|
||||
}];
|
||||
[self.clearButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.deleteButtonInternal.mas_bottom).offset(vSpace);
|
||||
make.left.right.equalTo(self.rightButtonContainer);
|
||||
make.height.equalTo(self.pasteButtonInternal);
|
||||
}];
|
||||
[self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace);
|
||||
make.left.right.equalTo(self.rightButtonContainer);
|
||||
// 允许在空间不足时缩短到 smallH,避免产生约束冲突
|
||||
make.height.greaterThanOrEqualTo(@(smallH));
|
||||
make.height.lessThanOrEqualTo(@(bigH));
|
||||
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom);
|
||||
}];
|
||||
|
||||
// 2. 粘贴区(位于右侧按钮左侧)
|
||||
[self addSubview:self.pasteViewInternal];
|
||||
[self.pasteViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.right.equalTo(self.rightButtonContainer.mas_left).offset(-12);
|
||||
make.top.equalTo(self.barViewInternal.mas_bottom).offset(8);
|
||||
make.height.mas_equalTo(48);
|
||||
}];
|
||||
|
||||
// 3. CollectionView
|
||||
[self addSubview:self.collectionViewInternal];
|
||||
[self.collectionViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.right.equalTo(self.rightButtonContainer.mas_left).offset(-12);
|
||||
make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(10);
|
||||
make.bottom.equalTo(self.mas_bottom).offset(-10);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Data
|
||||
|
||||
- (void)reloadDemoData {
|
||||
// 演示数据(可由外部替换)
|
||||
self.itemsInternal = @[@"高情商", @"暖味拉扯", @"风趣幽默", @"撩女生", @"社交惬匿", @"情场高手", @"一枚暖男", @"聊天搭子", @"表达爱意", @"更多话术"];
|
||||
[self.collectionViewInternal reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionView
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return self.itemsInternal.count;
|
||||
}
|
||||
|
||||
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
KBFunctionTagCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBFunctionTagCellId forIndexPath:indexPath];
|
||||
cell.titleLabel.text = self.itemsInternal[indexPath.item];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
// 三列等宽
|
||||
CGFloat totalW = collectionView.bounds.size.width;
|
||||
CGFloat space = 10.0;
|
||||
NSInteger columns = 3;
|
||||
CGFloat width = floor((totalW - space * (columns - 1)) / columns);
|
||||
return CGSizeMake(width, 48);
|
||||
}
|
||||
|
||||
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
|
||||
return 12.0;
|
||||
}
|
||||
|
||||
// 用户点击功能标签:优先 UL 拉起主App,失败再 Scheme;两次都失败则提示开启完全访问
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
|
||||
[KBHUD showInfo:@"处理中…"];
|
||||
// return;
|
||||
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) return;
|
||||
|
||||
NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @"";
|
||||
NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @"";
|
||||
|
||||
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)indexPath.item, encodedTitle]];
|
||||
if (!ul) return;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
|
||||
if (ok) return; // Universal Link 成功
|
||||
|
||||
// 统一使用主 App 注册的自定义 Scheme
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
|
||||
if (ok2) return;
|
||||
|
||||
// 兜底:在用户点击触发的场景下,尝试通过响应链调用 openURL:
|
||||
// 以提升在“备忘录”等宿主中的成功率。
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
|
||||
if (!bridged) {
|
||||
// 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager 引导。
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
|
||||
}
|
||||
}];
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Button Actions
|
||||
|
||||
- (void)onTapPaste {
|
||||
// 用户点击“粘贴”时才读取剪贴板:
|
||||
// - iOS16+ 会在跨 App 首次读取时自动弹出系统权限弹窗;
|
||||
// - iOS15 及以下不会弹窗,直接返回内容;
|
||||
// 注意:不要在非用户触发的时机主动读取(如 viewDidLoad),否则会造成“立刻弹窗”的体验。
|
||||
UIPasteboard *pb = [UIPasteboard generalPasteboard];
|
||||
NSString *text = pb.string; // 读取纯文本(可能触发系统粘贴权限弹窗)
|
||||
|
||||
if (text.length > 0) {
|
||||
// 将粘贴内容展示到左侧“粘贴区”的占位文案上
|
||||
self.pasteView.placeholderLabel.text = text;
|
||||
// 如果需要多行展示,可按需放开(高度由外部约束决定,默认一行会截断)
|
||||
// self.pasteView.placeholderLabel.numberOfLines = 0;
|
||||
} else {
|
||||
// 无可用文本或用户拒绝了粘贴权限;保持占位文案不变
|
||||
NSLog(@"粘贴板无可用文本或未授权粘贴");
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - 自动监控剪贴板(复制即弹窗)
|
||||
// 说明:
|
||||
// - 仅在视图可见时开启轮询,避免不必要的读取与打扰;
|
||||
// - 当检测到 changeCount 变化,立即读 pasteboard.string:
|
||||
// * iOS16+:此处会触发系统“是否允许粘贴”弹窗;
|
||||
// * iOS15:不会弹窗,直接得到文本;
|
||||
// - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。
|
||||
|
||||
- (void)startPasteboardMonitor {
|
||||
// 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志
|
||||
if (![[KBFullAccessManager shared] hasFullAccess]) return;
|
||||
if (self.pasteboardTimer) return;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
|
||||
UIPasteboard *pb = [UIPasteboard generalPasteboard];
|
||||
NSInteger cc = pb.changeCount;
|
||||
if (cc <= self.lastHandledPBCount) return; // 没有新复制
|
||||
self.lastHandledPBCount = cc; // 标记已处理,避免重复
|
||||
|
||||
// 实际读取触发系统弹窗(iOS16+)
|
||||
NSString *text = pb.string;
|
||||
if (text.length > 0) {
|
||||
self.pasteView.placeholderLabel.text = text;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)stopPasteboardMonitor {
|
||||
[self.pasteboardTimer invalidate];
|
||||
self.pasteboardTimer = nil;
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
[self kb_refreshPasteboardMonitor];
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden {
|
||||
BOOL wasHidden = self.isHidden;
|
||||
[super setHidden:hidden];
|
||||
if (wasHidden != hidden) {
|
||||
[self kb_refreshPasteboardMonitor];
|
||||
}
|
||||
}
|
||||
|
||||
// 根据窗口可见性与完全访问状态,统一启停粘贴板监控
|
||||
- (void)kb_refreshPasteboardMonitor {
|
||||
BOOL visible = (self.window && !self.isHidden);
|
||||
if (visible && [[KBFullAccessManager shared] hasFullAccess]) {
|
||||
[self startPasteboardMonitor];
|
||||
} else {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)kb_fullAccessChanged {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
|
||||
}
|
||||
- (void)onTapDelete {
|
||||
NSLog(@"点击:删除");
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
- (void)onTapClear {
|
||||
NSLog(@"点击:清空");
|
||||
// 连续删除:仅清空光标之前的输入(不改动 pasteView 的内容)
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
// 逐批读取 documentContextBeforeInput 并删除,避免 50 字符窗口限制带来的残留
|
||||
NSInteger guard = 0; // 上限保护,避免极端情况下长时间阻塞
|
||||
while (guard < 10000) {
|
||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||
NSInteger count = before.length;
|
||||
if (count <= 0) { break; } // 光标前已无内容
|
||||
for (NSInteger i = 0; i < count; i++) {
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
guard += count;
|
||||
}
|
||||
}
|
||||
- (void)onTapSend {
|
||||
NSLog(@"点击:发送");
|
||||
// 发送:插入换行。大多数聊天类 App 会把回车视为“发送”
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy insertText:@"\n"];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (KBFunctionBarView *)barViewInternal {
|
||||
if (!_barViewInternal) {
|
||||
_barViewInternal = [[KBFunctionBarView alloc] init];
|
||||
_barViewInternal.delegate = self; // 顶部功能Bar事件下发到本View
|
||||
}
|
||||
return _barViewInternal;
|
||||
}
|
||||
|
||||
#pragma mark - KBFunctionBarViewDelegate
|
||||
|
||||
- (void)functionBarView:(KBFunctionBarView *)bar didTapLeftAtIndex:(NSInteger)index {
|
||||
// 将事件继续透传给上层(如键盘控制器),用于切换界面或其它业务
|
||||
if ([self.delegate respondsToSelector:@selector(functionView:didTapToolActionAtIndex:)]) {
|
||||
[self.delegate functionView:self didTapToolActionAtIndex:index];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)functionBarView:(KBFunctionBarView *)bar didTapRightAtIndex:(NSInteger)index {
|
||||
// 右侧按钮点击,如收藏/宫格等,按需继续向外抛出(这里暂不定义单独协议方法,可在此内部处理或扩展)
|
||||
}
|
||||
|
||||
- (KBFunctionPasteView *)pasteViewInternal {
|
||||
if (!_pasteViewInternal) {
|
||||
_pasteViewInternal = [[KBFunctionPasteView alloc] init];
|
||||
}
|
||||
return _pasteViewInternal;
|
||||
}
|
||||
|
||||
- (UICollectionView *)collectionViewInternal {
|
||||
if (!_collectionViewInternal) {
|
||||
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
||||
layout.sectionInset = UIEdgeInsetsZero; // 外边距交由约束控制
|
||||
_collectionViewInternal = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
||||
_collectionViewInternal.backgroundColor = [UIColor clearColor];
|
||||
_collectionViewInternal.dataSource = self;
|
||||
_collectionViewInternal.delegate = self;
|
||||
[_collectionViewInternal registerClass:[KBFunctionTagCell class] forCellWithReuseIdentifier:kKBFunctionTagCellId];
|
||||
}
|
||||
return _collectionViewInternal;
|
||||
}
|
||||
|
||||
- (UIView *)rightButtonContainer {
|
||||
if (!_rightButtonContainer) {
|
||||
_rightButtonContainer = [[UIView alloc] init];
|
||||
_rightButtonContainer.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return _rightButtonContainer;
|
||||
}
|
||||
|
||||
- (UIButton *)buildRightButtonWithTitle:(NSString *)title color:(UIColor *)color {
|
||||
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
btn.backgroundColor = color;
|
||||
btn.layer.cornerRadius = 12.0;
|
||||
btn.layer.masksToBounds = YES;
|
||||
btn.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
[btn setTitle:title forState:UIControlStateNormal];
|
||||
[btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
return btn;
|
||||
}
|
||||
|
||||
- (UIButton *)pasteButtonInternal {
|
||||
if (!_pasteButtonInternal) {
|
||||
_pasteButtonInternal = [self buildRightButtonWithTitle:@"粘贴" color:[UIColor colorWithRed:0.13 green:0.73 blue:0.60 alpha:1.0]];
|
||||
[_pasteButtonInternal addTarget:self action:@selector(onTapPaste) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _pasteButtonInternal;
|
||||
}
|
||||
|
||||
- (UIButton *)deleteButtonInternal {
|
||||
if (!_deleteButtonInternal) {
|
||||
// 浅灰底深色文字,更接近截图里“删除”样式
|
||||
_deleteButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_deleteButtonInternal.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
|
||||
_deleteButtonInternal.layer.cornerRadius = 12.0;
|
||||
_deleteButtonInternal.layer.masksToBounds = YES;
|
||||
_deleteButtonInternal.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
[_deleteButtonInternal setTitle:@"删除" forState:UIControlStateNormal];
|
||||
[_deleteButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
[_deleteButtonInternal addTarget:self action:@selector(onTapDelete) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _deleteButtonInternal;
|
||||
}
|
||||
|
||||
- (UIButton *)clearButtonInternal {
|
||||
if (!_clearButtonInternal) {
|
||||
_clearButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_clearButtonInternal.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
|
||||
_clearButtonInternal.layer.cornerRadius = 12.0;
|
||||
_clearButtonInternal.layer.masksToBounds = YES;
|
||||
_clearButtonInternal.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
[_clearButtonInternal setTitle:@"清空" forState:UIControlStateNormal];
|
||||
[_clearButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
[_clearButtonInternal addTarget:self action:@selector(onTapClear) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _clearButtonInternal;
|
||||
}
|
||||
|
||||
- (UIButton *)sendButtonInternal {
|
||||
if (!_sendButtonInternal) {
|
||||
_sendButtonInternal = [self buildRightButtonWithTitle:@"发送" color:[UIColor colorWithRed:0.13 green:0.73 blue:0.60 alpha:1.0]];
|
||||
[_sendButtonInternal addTarget:self action:@selector(onTapSend) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _sendButtonInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Expose
|
||||
|
||||
- (UICollectionView *)collectionView { return self.collectionViewInternal; }
|
||||
- (NSArray<NSString *> *)items { return self.itemsInternal; }
|
||||
- (KBFunctionBarView *)barView { return self.barViewInternal; }
|
||||
- (KBFunctionPasteView *)pasteView { return self.pasteViewInternal; }
|
||||
- (UIButton *)pasteButton { return self.pasteButtonInternal; }
|
||||
- (UIButton *)deleteButton { return self.deleteButtonInternal; }
|
||||
- (UIButton *)clearButton { return self.clearButtonInternal; }
|
||||
- (UIButton *)sendButton { return self.sendButtonInternal; }
|
||||
|
||||
#pragma mark - Find Owner Controller
|
||||
|
||||
// 工具方法已提取到 KBResponderUtils.h
|
||||
|
||||
@end
|
||||
36
CustomKeyboard/View/KBKeyBoardMainView.h
Normal file
36
CustomKeyboard/View/KBKeyBoardMainView.h
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// KBKeyBoardMainView.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
|
||||
@class KBKeyBoardMainView, KBKey;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol KBKeyBoardMainViewDelegate <NSObject>
|
||||
@optional
|
||||
/// 键被点击的回调
|
||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key;
|
||||
|
||||
/// 顶部工具栏按钮点击回调(index: 0~3)。
|
||||
/// 需求:当 index == 0 时,由外部(KeyboardViewController)决定是否切换到功能面板
|
||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index;
|
||||
|
||||
/// 点击了右侧设置按钮
|
||||
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView;
|
||||
@end
|
||||
|
||||
@interface KBKeyBoardMainView : UIView
|
||||
@property (nonatomic, weak) id<KBKeyBoardMainViewDelegate> delegate;
|
||||
|
||||
/// 应用当前皮肤(会触发键区重载以应用按键颜色)
|
||||
- (void)kb_applyTheme;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
129
CustomKeyboard/View/KBKeyBoardMainView.m
Normal file
129
CustomKeyboard/View/KBKeyBoardMainView.m
Normal file
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// KBKeyBoardMainView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import "KBKeyBoardMainView.h"
|
||||
#import "KBToolBar.h"
|
||||
#import "KBKeyboardView.h"
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBKey.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate>
|
||||
@property (nonatomic, strong) KBToolBar *topBar;
|
||||
@property (nonatomic, strong) KBKeyboardView *keyboardView;
|
||||
// 注意:功能面板的展示/隐藏由外部控制器决定,此处不再直接管理显隐
|
||||
@end
|
||||
@implementation KBKeyBoardMainView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
|
||||
// 顶部栏
|
||||
self.topBar = [[KBToolBar alloc] init];
|
||||
self.topBar.delegate = self;
|
||||
[self addSubview:self.topBar];
|
||||
[self.topBar mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self);
|
||||
make.top.equalTo(self.mas_top).offset(6);
|
||||
make.height.mas_equalTo(40);
|
||||
}];
|
||||
|
||||
// 键盘区域
|
||||
self.keyboardView = [[KBKeyboardView alloc] init];
|
||||
self.keyboardView.delegate = self;
|
||||
[self addSubview:self.keyboardView];
|
||||
[self.keyboardView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.right.equalTo(self);
|
||||
make.top.equalTo(self.topBar.mas_bottom).offset(4);
|
||||
make.bottom.equalTo(self.mas_bottom).offset(-4);
|
||||
}];
|
||||
|
||||
// 功能面板切换交由外部控制器处理;此处不直接创建/管理
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - KBToolBarDelegate
|
||||
|
||||
- (void)toolBar:(KBToolBar *)toolBar didTapActionAtIndex:(NSInteger)index {
|
||||
// 将事件抛给外部控制器,由其决定是否切换到功能面板
|
||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapToolActionAtIndex:)]) {
|
||||
[self.delegate keyBoardMainView:self didTapToolActionAtIndex:index];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toolBarDidTapSettings:(KBToolBar *)toolBar {
|
||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainViewDidTapSettings:)]) {
|
||||
[self.delegate keyBoardMainViewDidTapSettings:self];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - KBKeyboardViewDelegate
|
||||
|
||||
- (void)keyboardView:(KBKeyboardView *)keyboard didTapKey:(KBKey *)key {
|
||||
switch (key.type) {
|
||||
case KBKeyTypeCharacter:
|
||||
// 文本插入交由上层控制器处理
|
||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
|
||||
[self.delegate keyBoardMainView:self didTapKey:key];
|
||||
}
|
||||
break;
|
||||
case KBKeyTypeBackspace:
|
||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
|
||||
[self.delegate keyBoardMainView:self didTapKey:key];
|
||||
}
|
||||
break;
|
||||
case KBKeyTypeSpace:
|
||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
|
||||
[self.delegate keyBoardMainView:self didTapKey:key];
|
||||
}
|
||||
break;
|
||||
case KBKeyTypeReturn:
|
||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
|
||||
[self.delegate keyBoardMainView:self didTapKey:key];
|
||||
}
|
||||
break;
|
||||
case KBKeyTypeModeChange: {
|
||||
// 切换 字母 <-> 数字 布局
|
||||
keyboard.layoutStyle = (keyboard.layoutStyle == KBKeyboardLayoutStyleLetters) ? KBKeyboardLayoutStyleNumbers : KBKeyboardLayoutStyleLetters;
|
||||
[keyboard reloadKeys];
|
||||
} break;
|
||||
case KBKeyTypeGlobe:
|
||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
|
||||
[self.delegate keyBoardMainView:self didTapKey:key];
|
||||
}
|
||||
break;
|
||||
case KBKeyTypeCustom:
|
||||
// 自定义占位:切换语言或其它操作
|
||||
if ([self.delegate respondsToSelector:@selector(keyBoardMainView:didTapKey:)]) {
|
||||
[self.delegate keyBoardMainView:self didTapKey:key];
|
||||
}
|
||||
break;
|
||||
case KBKeyTypeShift:
|
||||
// Shift 已在 KBKeyboardView 内部处理
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换功能面板交由外部控制器处理(此处不再实现)
|
||||
|
||||
// 设置页展示改由 KeyboardViewController 统一处理
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)kb_applyTheme {
|
||||
KBSkinManager *mgr = [KBSkinManager shared];
|
||||
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
|
||||
UIColor *bg = mgr.current.keyboardBackground;
|
||||
self.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
||||
self.keyboardView.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
||||
[self.keyboardView reloadKeys];
|
||||
}
|
||||
|
||||
@end
|
||||
20
CustomKeyboard/View/KBKeyButton.h
Normal file
20
CustomKeyboard/View/KBKeyButton.h
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// KBKeyButton.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@class KBKey;
|
||||
|
||||
/// 自定义键按钮(UIButton 子类):圆角外观,按下高亮效果。
|
||||
@interface KBKeyButton : UIButton
|
||||
|
||||
@property (nonatomic, strong) KBKey *key;
|
||||
|
||||
/// 配置基础样式(背景、圆角等)。创建按钮时调用。
|
||||
- (void)applyDefaultStyle;
|
||||
|
||||
/// 根据选中/高亮等状态刷新外观
|
||||
- (void)refreshStateAppearance;
|
||||
|
||||
@end
|
||||
59
CustomKeyboard/View/KBKeyButton.m
Normal file
59
CustomKeyboard/View/KBKeyButton.m
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// KBKeyButton.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBKeyButton.h"
|
||||
#import "KBKey.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@implementation KBKeyButton
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self applyDefaultStyle];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)applyDefaultStyle {
|
||||
self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
|
||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||
[self setTitleColor:t.keyTextColor forState:UIControlStateNormal];
|
||||
[self setTitleColor:t.keyTextColor forState:UIControlStateHighlighted];
|
||||
self.backgroundColor = t.keyBackground;
|
||||
self.layer.cornerRadius = 6.0; // 圆角
|
||||
self.layer.masksToBounds = NO;
|
||||
self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; // 阴影效果
|
||||
self.layer.shadowOpacity = 1.0;
|
||||
self.layer.shadowOffset = CGSizeMake(0, 1);
|
||||
self.layer.shadowRadius = 1.5;
|
||||
[self refreshStateAppearance];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted {
|
||||
[super setHighlighted:highlighted];
|
||||
// 简单按压反馈:选中态不改变透明度,避免和高亮态冲突
|
||||
if (self.isSelected) {
|
||||
self.alpha = 1.0;
|
||||
} else {
|
||||
self.alpha = highlighted ? 0.2 : 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected {
|
||||
[super setSelected:selected];
|
||||
[self refreshStateAppearance];
|
||||
}
|
||||
|
||||
- (void)refreshStateAppearance {
|
||||
// 选中态用于 Shift/CapsLock 等特殊按键的高亮显示
|
||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||
if (self.isSelected) {
|
||||
self.backgroundColor = t.keyHighlightBackground ?: t.keyBackground;
|
||||
} else {
|
||||
self.backgroundColor = t.keyBackground;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
33
CustomKeyboard/View/KBKeyboardView.h
Normal file
33
CustomKeyboard/View/KBKeyboardView.h
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// KBKeyboardView.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// 键盘主容器,内部管理按键行布局。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class KBKeyboardView, KBKey;
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBKeyboardLayoutStyle) {
|
||||
KBKeyboardLayoutStyleLetters = 0,
|
||||
KBKeyboardLayoutStyleNumbers
|
||||
};
|
||||
|
||||
@protocol KBKeyboardViewDelegate <NSObject>
|
||||
@optional
|
||||
/// 键被点击的回调
|
||||
- (void)keyboardView:(KBKeyboardView *)keyboard didTapKey:(KBKey *)key;
|
||||
@end
|
||||
|
||||
@interface KBKeyboardView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<KBKeyboardViewDelegate> delegate;
|
||||
@property (nonatomic, assign) KBKeyboardLayoutStyle layoutStyle; // 布局样式(字母/数字)
|
||||
@property (nonatomic, assign, getter=isShiftOn) BOOL shiftOn; // 大小写状态
|
||||
// 在数字布局中,是否显示“更多符号”(#+=)页
|
||||
@property (nonatomic, assign) BOOL symbolsMoreOn;
|
||||
|
||||
- (void)reloadKeys; // 当布局样式/大小写变化时调用
|
||||
|
||||
@end
|
||||
356
CustomKeyboard/View/KBKeyboardView.m
Normal file
356
CustomKeyboard/View/KBKeyboardView.m
Normal file
@@ -0,0 +1,356 @@
|
||||
//
|
||||
// KBKeyboardView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBKeyboardView.h"
|
||||
#import "KBKeyButton.h"
|
||||
#import "KBKey.h"
|
||||
#import "KBResponderUtils.h" // 封装的响应链工具
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@interface KBKeyboardView ()
|
||||
@property (nonatomic, strong) UIView *row1;
|
||||
@property (nonatomic, strong) UIView *row2;
|
||||
@property (nonatomic, strong) UIView *row3;
|
||||
@property (nonatomic, strong) UIView *row4;
|
||||
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
|
||||
// 长按退格的一次次删除控制标记(不使用 NSTimer,仅用 GCD 递归调度)
|
||||
@property (nonatomic, assign) BOOL backspaceHoldActive;
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
|
||||
_layoutStyle = KBKeyboardLayoutStyleLetters;
|
||||
// 默认小写:与需求一致,初始不开启 Shift
|
||||
_shiftOn = NO;
|
||||
_symbolsMoreOn = NO; // 数字面板默认第一页(123)
|
||||
[self buildBase];
|
||||
[self reloadKeys];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// 当切换大布局(字母/数字)时,重置数字二级页状态
|
||||
- (void)setLayoutStyle:(KBKeyboardLayoutStyle)layoutStyle {
|
||||
_layoutStyle = layoutStyle;
|
||||
if (_layoutStyle != KBKeyboardLayoutStyleNumbers) {
|
||||
_symbolsMoreOn = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)buildBase {
|
||||
[self addSubview:self.row1];
|
||||
[self addSubview:self.row2];
|
||||
[self addSubview:self.row3];
|
||||
[self addSubview:self.row4];
|
||||
|
||||
CGFloat vSpacing = 8;
|
||||
[self.row1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.mas_top).offset(8);
|
||||
make.left.right.equalTo(self);
|
||||
make.height.mas_equalTo(44);
|
||||
}];
|
||||
[self.row2 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.row1.mas_bottom).offset(vSpacing);
|
||||
make.left.right.equalTo(self);
|
||||
make.height.equalTo(self.row1);
|
||||
}];
|
||||
[self.row3 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.row2.mas_bottom).offset(vSpacing);
|
||||
make.left.right.equalTo(self);
|
||||
make.height.equalTo(self.row1);
|
||||
}];
|
||||
[self.row4 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.row3.mas_bottom).offset(vSpacing);
|
||||
make.left.right.equalTo(self);
|
||||
make.height.equalTo(self.row1);
|
||||
make.bottom.equalTo(self.mas_bottom).offset(-6);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)reloadKeys {
|
||||
// 移除旧按钮
|
||||
for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) {
|
||||
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||
}
|
||||
|
||||
self.keysForRows = [self buildKeysForCurrentLayout];
|
||||
[self buildRow:self.row1 withKeys:self.keysForRows[0]];
|
||||
// 第二行:字母布局时通过左右等宽占位让整行居中
|
||||
CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters) ? 0.5 : 0.0;
|
||||
[self buildRow:self.row2 withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer];
|
||||
[self buildRow:self.row3 withKeys:self.keysForRows[2]];
|
||||
[self buildRow:self.row4 withKeys:self.keysForRows[3]];
|
||||
}
|
||||
|
||||
- (NSArray<NSArray<KBKey *> *> *)buildKeysForCurrentLayout {
|
||||
if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) {
|
||||
// 数字/符号布局:3 行主键 + 底部控制行
|
||||
NSArray *r1 = nil;
|
||||
NSArray *r2 = nil;
|
||||
NSArray *r3 = nil;
|
||||
|
||||
if (!self.symbolsMoreOn) {
|
||||
// 数字第一页(123)
|
||||
r1 = @[ [KBKey keyWithTitle:@"1" output:@"1"], [KBKey keyWithTitle:@"2" output:@"2"], [KBKey keyWithTitle:@"3" output:@"3"],
|
||||
[KBKey keyWithTitle:@"4" output:@"4"], [KBKey keyWithTitle:@"5" output:@"5"], [KBKey keyWithTitle:@"6" output:@"6"],
|
||||
[KBKey keyWithTitle:@"7" output:@"7"], [KBKey keyWithTitle:@"8" output:@"8"], [KBKey keyWithTitle:@"9" output:@"9"], [KBKey keyWithTitle:@"0" output:@"0"] ];
|
||||
r2 = @[ [KBKey keyWithTitle:@"-" output:@"-"], [KBKey keyWithTitle:@"/" output:@"/"], [KBKey keyWithTitle:@":" output:@":"],
|
||||
[KBKey keyWithTitle:@";" output:@";"], [KBKey keyWithTitle:@"(" output:@"("], [KBKey keyWithTitle:@")" output:@")"],
|
||||
[KBKey keyWithTitle:@"$" output:@"$"], [KBKey keyWithTitle:@"&" output:@"&"], [KBKey keyWithTitle:@"@" output:@"@"], [KBKey keyWithTitle:@"\"" output:@"\""] ];
|
||||
r3 = @[ [KBKey keyWithTitle:@"#+=" type:KBKeyTypeSymbolsToggle],
|
||||
[KBKey keyWithTitle:@"," output:@","], [KBKey keyWithTitle:@"." output:@"."], [KBKey keyWithTitle:@"?" output:@"?"],
|
||||
[KBKey keyWithTitle:@"!" output:@"!"], [KBKey keyWithTitle:@"'" output:@"'"],
|
||||
[KBKey keyWithTitle:@"⌫" type:KBKeyTypeBackspace] ];
|
||||
} else {
|
||||
// 数字第二页(#+=):前两行替换为更多符号,左下角按钮文案改为“123”
|
||||
r1 = @[ [KBKey keyWithTitle:@"[" output:@"["], [KBKey keyWithTitle:@"]" output:@"]"], [KBKey keyWithTitle:@"{" output:@"{"],
|
||||
[KBKey keyWithTitle:@"}" output:@"}"], [KBKey keyWithTitle:@"#" output:@"#"], [KBKey keyWithTitle:@"%" output:@"%"],
|
||||
[KBKey keyWithTitle:@"^" output:@"^"], [KBKey keyWithTitle:@"*" output:@"*"], [KBKey keyWithTitle:@"+" output:@"+"],
|
||||
[KBKey keyWithTitle:@"=" output:@"="] ];
|
||||
r2 = @[ [KBKey keyWithTitle:@"_" output:@"_"], [KBKey keyWithTitle:@"\\" output:@"\\"], [KBKey keyWithTitle:@"|" output:@"|"],
|
||||
[KBKey keyWithTitle:@"~" output:@"~"], [KBKey keyWithTitle:@"<" output:@"<"], [KBKey keyWithTitle:@">" output:@">"],
|
||||
[KBKey keyWithTitle:@"$" output:@"$"], [KBKey keyWithTitle:@"€" output:@"€"], [KBKey keyWithTitle:@"£" output:@"£"],
|
||||
[KBKey keyWithTitle:@"•" output:@"•"] ];
|
||||
r3 = @[ [KBKey keyWithTitle:@"123" type:KBKeyTypeSymbolsToggle],
|
||||
[KBKey keyWithTitle:@"," output:@","], [KBKey keyWithTitle:@"." output:@"."], [KBKey keyWithTitle:@"?" output:@"?"],
|
||||
[KBKey keyWithTitle:@"!" output:@"!"], [KBKey keyWithTitle:@"'" output:@"'"],
|
||||
[KBKey keyWithTitle:@"⌫" type:KBKeyTypeBackspace] ];
|
||||
}
|
||||
|
||||
NSArray *r4 = @[ [KBKey keyWithTitle:@"abc" type:KBKeyTypeModeChange],
|
||||
[KBKey keyWithTitle:@"AI" type:KBKeyTypeCustom],
|
||||
[KBKey keyWithTitle:@"space" type:KBKeyTypeSpace],
|
||||
[KBKey keyWithTitle:@"发送" type:KBKeyTypeReturn] ];
|
||||
|
||||
return @[r1, r2, r3, r4];
|
||||
}
|
||||
|
||||
// 字母布局(QWERTY)
|
||||
NSArray *r1 = @[ @"Q", @"W", @"E", @"R", @"T", @"Y", @"U", @"I", @"O", @"P" ];
|
||||
NSArray *r2 = @[ @"A", @"S", @"D", @"F", @"G", @"H", @"J", @"K", @"L" ];
|
||||
NSArray *r3chars = @[ @"Z", @"X", @"C", @"V", @"B", @"N", @"M" ];
|
||||
|
||||
NSMutableArray *row1 = [NSMutableArray arrayWithCapacity:r1.count];
|
||||
// 字母键标题与输出同时随 Shift 切换大小写,界面与输入保持一致
|
||||
for (NSString *s in r1) {
|
||||
NSString *shown = self.shiftOn ? s : s.lowercaseString;
|
||||
[row1 addObject:[KBKey keyWithTitle:shown output:shown]];
|
||||
}
|
||||
|
||||
NSMutableArray *row2 = [NSMutableArray arrayWithCapacity:r2.count];
|
||||
for (NSString *s in r2) {
|
||||
NSString *shown = self.shiftOn ? s : s.lowercaseString;
|
||||
[row2 addObject:[KBKey keyWithTitle:shown output:shown]];
|
||||
}
|
||||
|
||||
NSMutableArray *row3 = [NSMutableArray array];
|
||||
[row3 addObject:[KBKey keyWithTitle:@"⇧" type:KBKeyTypeShift]];
|
||||
for (NSString *s in r3chars) {
|
||||
NSString *shown = self.shiftOn ? s : s.lowercaseString;
|
||||
[row3 addObject:[KBKey keyWithTitle:shown output:shown]];
|
||||
}
|
||||
[row3 addObject:[KBKey keyWithTitle:@"⌫" type:KBKeyTypeBackspace]];
|
||||
|
||||
NSArray *row4 = @[ [KBKey keyWithTitle:@"123" type:KBKeyTypeModeChange],
|
||||
[KBKey keyWithTitle:@"AI" type:KBKeyTypeCustom],
|
||||
[KBKey keyWithTitle:@"space" type:KBKeyTypeSpace],
|
||||
[KBKey keyWithTitle:@"发送" type:KBKeyTypeReturn] ];
|
||||
|
||||
return @[row1.copy, row2.copy, row3.copy, row4];
|
||||
}
|
||||
|
||||
- (void)buildRow:(UIView *)row withKeys:(NSArray<KBKey *> *)keys {
|
||||
[self buildRow:row withKeys:keys edgeSpacerMultiplier:0.0];
|
||||
}
|
||||
|
||||
- (void)buildRow:(UIView *)row withKeys:(NSArray<KBKey *> *)keys edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
|
||||
CGFloat hInset = 6; // 行左右内边距
|
||||
CGFloat spacing = 6; // 键与键之间的间距
|
||||
UIView *previous = nil;
|
||||
UIView *leftSpacer = nil;
|
||||
UIView *rightSpacer = nil;
|
||||
if (edgeSpacerMultiplier > 0.0) {
|
||||
leftSpacer = [UIView new];
|
||||
rightSpacer = [UIView new];
|
||||
leftSpacer.backgroundColor = [UIColor clearColor];
|
||||
rightSpacer.backgroundColor = [UIColor clearColor];
|
||||
[row addSubview:leftSpacer];
|
||||
[row addSubview:rightSpacer];
|
||||
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(row.mas_left).offset(hInset);
|
||||
make.centerY.equalTo(row);
|
||||
make.height.mas_equalTo(1);
|
||||
}];
|
||||
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(row.mas_right).offset(-hInset);
|
||||
make.centerY.equalTo(row);
|
||||
make.height.mas_equalTo(1);
|
||||
}];
|
||||
}
|
||||
for (NSInteger i = 0; i < keys.count; i++) {
|
||||
KBKey *key = keys[i];
|
||||
KBKeyButton *btn = [[KBKeyButton alloc] init];
|
||||
btn.key = key;
|
||||
[btn setTitle:key.title forState:UIControlStateNormal];
|
||||
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[row addSubview:btn];
|
||||
|
||||
// ⌫ 长按:开始连续逐个删除(无需 NSTimer)。使用 UILongPressGestureRecognizer 识别长按,
|
||||
// 在开始态触发递归的轻量调度,每次删除 1 个字符,直到松手或无内容。
|
||||
if (key.type == KBKeyTypeBackspace) {
|
||||
UILongPressGestureRecognizer *lp = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onBackspaceLongPress:)];
|
||||
// 稍短的判定时间,提升响应(默认约 0.5s)。
|
||||
lp.minimumPressDuration = 0.35;
|
||||
lp.cancelsTouchesInView = YES; // 被识别为长按时,取消普通点击
|
||||
[btn addGestureRecognizer:lp];
|
||||
}
|
||||
|
||||
// Shift 按钮选中态随大小写状态变化
|
||||
if (key.type == KBKeyTypeShift) {
|
||||
btn.selected = self.shiftOn;
|
||||
}
|
||||
|
||||
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.bottom.equalTo(row);
|
||||
if (previous) {
|
||||
make.left.equalTo(previous.mas_right).offset(spacing);
|
||||
} else {
|
||||
if (leftSpacer) {
|
||||
make.left.equalTo(leftSpacer.mas_right).offset(spacing);
|
||||
} else {
|
||||
make.left.equalTo(row.mas_left).offset(hInset);
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
// 宽度规则:字符键等宽;特殊键按倍数放大
|
||||
if (key.type == KBKeyTypeCharacter) {
|
||||
if (previous && previous != nil) {
|
||||
if (((KBKeyButton *)previous).key.type == KBKeyTypeCharacter) {
|
||||
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.equalTo(previous);
|
||||
}];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// special keys: give 1.5x of a character key by deferring constraint equalities after loop
|
||||
}
|
||||
|
||||
previous = btn;
|
||||
}
|
||||
// 右侧使用内边距或右占位
|
||||
[previous mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
if (rightSpacer) {
|
||||
make.right.equalTo(rightSpacer.mas_left).offset(-spacing);
|
||||
} else {
|
||||
make.right.equalTo(row.mas_right).offset(-hInset);
|
||||
}
|
||||
}];
|
||||
|
||||
// 第二遍:以首个字符键为基准,统一设置特殊键宽度倍数
|
||||
KBKeyButton *firstChar = nil;
|
||||
for (KBKeyButton *b in row.subviews) {
|
||||
if ([b isKindOfClass:[KBKeyButton class]] && b.key.type == KBKeyTypeCharacter) { firstChar = b; break; }
|
||||
}
|
||||
// 若该行没有字符键(例如底部控制行),则使用行内第一个按钮作为基准宽度
|
||||
if (!firstChar) {
|
||||
for (KBKeyButton *b in row.subviews) { if ([b isKindOfClass:[KBKeyButton class]]) { firstChar = b; break; } }
|
||||
}
|
||||
if (firstChar) {
|
||||
for (KBKeyButton *b in row.subviews) {
|
||||
if (![b isKindOfClass:[KBKeyButton class]]) continue;
|
||||
// 当本行没有字符键时,firstChar 可能是一个“特殊键”,
|
||||
// 避免对基准按钮自身添加 self == self * k 的无效约束
|
||||
if (b == firstChar) continue;
|
||||
if (b.key.type == KBKeyTypeCharacter) continue;
|
||||
CGFloat multiplier = 1.5;
|
||||
if (b.key.type == KBKeyTypeSpace) multiplier = 4.0;
|
||||
if (b.key.type == KBKeyTypeReturn) multiplier = 1.8;
|
||||
if (b.key.type == KBKeyTypeModeChange || b.key.type == KBKeyTypeGlobe || b.key.type == KBKeyTypeShift || b.key.type == KBKeyTypeBackspace) {
|
||||
multiplier = 1.5;
|
||||
}
|
||||
[b mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.equalTo(firstChar).multipliedBy(multiplier);
|
||||
}];
|
||||
}
|
||||
// 如果有左右占位,则把占位宽度设置为字符键宽度的一定倍数,以实现整体居中
|
||||
if (leftSpacer && rightSpacer) {
|
||||
[leftSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.equalTo(firstChar).multipliedBy(edgeSpacerMultiplier);
|
||||
}];
|
||||
[rightSpacer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.equalTo(firstChar).multipliedBy(edgeSpacerMultiplier);
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onKeyTapped:(KBKeyButton *)sender {
|
||||
KBKey *key = sender.key;
|
||||
if (key.type == KBKeyTypeShift) {
|
||||
self.shiftOn = !self.shiftOn;
|
||||
[self reloadKeys];
|
||||
return;
|
||||
}
|
||||
if (key.type == KBKeyTypeSymbolsToggle) {
|
||||
// 在数字布局内切换 123 <-> #+=
|
||||
self.symbolsMoreOn = !self.symbolsMoreOn;
|
||||
[self reloadKeys];
|
||||
return;
|
||||
}
|
||||
if ([self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) {
|
||||
[self.delegate keyboardView:self didTapKey:key];
|
||||
}
|
||||
}
|
||||
|
||||
// 长按退格:按住时以小间隔逐个删除;松手停止。(不使用 NSTimer/DisplayLink)
|
||||
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
|
||||
switch (gr.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
self.backspaceHoldActive = YES;
|
||||
[self kb_backspaceStep];
|
||||
} break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed: {
|
||||
self.backspaceHoldActive = NO;
|
||||
} break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
// 单步删除并在需要时安排下一次,直到松手或无内容
|
||||
- (void)kb_backspaceStep {
|
||||
if (!self.backspaceHoldActive) { return; }
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||
if (before.length <= 0) { self.backspaceHoldActive = NO; return; }
|
||||
[proxy deleteBackward]; // 每次仅删 1 个
|
||||
|
||||
// 轻量递归调度下一次;不使用 NSTimer,避免复杂状态管理
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.06 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
__strong typeof(weakSelf) selfStrong = weakSelf;
|
||||
[selfStrong kb_backspaceStep];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; }
|
||||
- (UIView *)row2 { if (!_row2) _row2 = [UIView new]; return _row2; }
|
||||
- (UIView *)row3 { if (!_row3) _row3 = [UIView new]; return _row3; }
|
||||
- (UIView *)row4 { if (!_row4) _row4 = [UIView new]; return _row4; }
|
||||
|
||||
@end
|
||||
28
CustomKeyboard/View/KBResponderUtils.h
Normal file
28
CustomKeyboard/View/KBResponderUtils.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// KBResponderUtils.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// 统一封装:从任意 UIView/UIResponder 起,向响应链上查找 UIInputViewController。
|
||||
// 作为 header‑only 的工具,便于多处直接引入使用。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#ifndef KBResponderUtils_h
|
||||
#define KBResponderUtils_h
|
||||
|
||||
/// 从给定 responder 开始,沿响应链查找宿主 UIInputViewController。
|
||||
/// 用法:UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
static inline UIInputViewController *KBFindInputViewController(UIResponder *start) {
|
||||
UIResponder *responder = start;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#endif /* KBResponderUtils_h */
|
||||
|
||||
20
CustomKeyboard/View/KBSettingView.h
Normal file
20
CustomKeyboard/View/KBSettingView.h
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// KBSettingView.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// 简单的设置页面:左上角返回箭头按钮 + 占位内容区域。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KBSettingView : UIView
|
||||
|
||||
/// 左上角返回按钮(外部添加 target 实现返回)
|
||||
@property (nonatomic, strong, readonly) UIButton *backButton;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
71
CustomKeyboard/View/KBSettingView.m
Normal file
71
CustomKeyboard/View/KBSettingView.m
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// KBSettingView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBSettingView.h"
|
||||
#import "Masonry.h"
|
||||
|
||||
@interface KBSettingView ()
|
||||
@property (nonatomic, strong) UIButton *backButtonInternal;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@end
|
||||
|
||||
@implementation KBSettingView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
// 背景做成淡色,和主界面区分
|
||||
self.backgroundColor = [UIColor colorWithWhite:1 alpha:0.96];
|
||||
|
||||
[self addSubview:self.backButtonInternal];
|
||||
[self.backButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(10);
|
||||
make.top.equalTo(self.mas_top).offset(8);
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
self.titleLabel = [[UILabel alloc] init];
|
||||
self.titleLabel.text = @"设置";
|
||||
self.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
self.titleLabel.textColor = [UIColor blackColor];
|
||||
[self addSubview:self.titleLabel];
|
||||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerY.equalTo(self.backButtonInternal.mas_centerY);
|
||||
make.centerX.equalTo(self.mas_centerX);
|
||||
}];
|
||||
|
||||
// 占位内容
|
||||
UILabel *place = [[UILabel alloc] init];
|
||||
place.text = @"这里是设置内容占位";
|
||||
place.textColor = [UIColor darkGrayColor];
|
||||
place.font = [UIFont systemFontOfSize:14];
|
||||
[self addSubview:place];
|
||||
[place mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.equalTo(self);
|
||||
}];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIButton *)backButtonInternal {
|
||||
if (!_backButtonInternal) {
|
||||
_backButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_backButtonInternal.layer.cornerRadius = 16;
|
||||
_backButtonInternal.layer.masksToBounds = YES;
|
||||
_backButtonInternal.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
[_backButtonInternal setTitle:@"←" forState:UIControlStateNormal]; // 返回箭头
|
||||
[_backButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
_backButtonInternal.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
||||
}
|
||||
return _backButtonInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Expose
|
||||
|
||||
- (UIButton *)backButton { return self.backButtonInternal; }
|
||||
|
||||
@end
|
||||
|
||||
@@ -2,15 +2,35 @@
|
||||
// KBToolBar.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/27.
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class KBToolBar;
|
||||
|
||||
@protocol KBToolBarDelegate <NSObject>
|
||||
@optional
|
||||
/// 左侧 4 个功能按钮点击(index: 0~3)
|
||||
- (void)toolBar:(KBToolBar *)toolBar didTapActionAtIndex:(NSInteger)index;
|
||||
/// 右侧设置按钮点击
|
||||
- (void)toolBarDidTapSettings:(KBToolBar *)toolBar;
|
||||
@end
|
||||
|
||||
/// 顶部工具栏:左侧 4 个按钮,右侧 1 个设置按钮。
|
||||
@interface KBToolBar : UIView
|
||||
|
||||
@property (nonatomic, weak, nullable) id<KBToolBarDelegate> delegate;
|
||||
|
||||
/// 左侧 4 个按钮的标题。默认值:@[@"Item1", @"Item2", @"Item3", @"Item4"]。
|
||||
@property (nonatomic, copy) NSArray<NSString *> *leftButtonTitles;
|
||||
|
||||
/// 暴露按钮以便外部定制(只读;首次访问时懒加载创建)
|
||||
@property (nonatomic, strong, readonly) NSArray<UIButton *> *leftButtons;
|
||||
@property (nonatomic, strong, readonly) UIButton *settingsButton;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -2,20 +2,212 @@
|
||||
// KBToolBar.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/27.
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import "KBToolBar.h"
|
||||
#import "KBResponderUtils.h" // 查找 UIInputViewController,用于系统切换输入法
|
||||
|
||||
@interface KBToolBar ()
|
||||
@property (nonatomic, strong) UIView *leftContainer;
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
||||
@property (nonatomic, strong) UIButton *settingsButtonInternal;
|
||||
@property (nonatomic, strong) UIButton *globeButtonInternal; // 可选:系统“切换输入法”键
|
||||
@end
|
||||
|
||||
@implementation KBToolBar
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
_leftButtonTitles = @[@"Item1", @"Item2", @"Item3", @"Item4"]; // 默认标题
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSArray<UIButton *> *)leftButtons {
|
||||
return self.leftButtonsInternal;
|
||||
}
|
||||
|
||||
- (UIButton *)settingsButton {
|
||||
return self.settingsButtonInternal;
|
||||
}
|
||||
|
||||
- (void)setLeftButtonTitles:(NSArray<NSString *> *)leftButtonTitles {
|
||||
_leftButtonTitles = [leftButtonTitles copy];
|
||||
// Update titles if buttons already exist
|
||||
[self.leftButtonsInternal enumerateObjectsUsingBlock:^(UIButton * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
if (idx < self.leftButtonTitles.count) {
|
||||
[obj setTitle:self.leftButtonTitles[idx] forState:UIControlStateNormal];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - 视图搭建
|
||||
|
||||
- (void)setupUI {
|
||||
[self addSubview:self.leftContainer];
|
||||
[self addSubview:self.settingsButtonInternal];
|
||||
[self addSubview:self.globeButtonInternal];
|
||||
|
||||
// 右侧设置按钮
|
||||
[self.settingsButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.mas_right).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 左侧地球键(按需显示)
|
||||
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 左侧容器占用剩余空间
|
||||
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
make.right.equalTo(self.settingsButtonInternal.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 在左侧容器中创建 4 个等宽按钮
|
||||
NSMutableArray<UIButton *> *buttons = [NSMutableArray arrayWithCapacity:4];
|
||||
UIView *previous = nil;
|
||||
for (NSInteger i = 0; i < 4; i++) {
|
||||
UIButton *btn = [self buildActionButtonAtIndex:i];
|
||||
[self.leftContainer addSubview:btn];
|
||||
[buttons addObject:btn];
|
||||
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
if (previous) {
|
||||
make.left.equalTo(previous.mas_right).offset(8);
|
||||
make.width.equalTo(previous);
|
||||
} else {
|
||||
make.left.equalTo(self.leftContainer.mas_left);
|
||||
}
|
||||
make.top.bottom.equalTo(self.leftContainer);
|
||||
}];
|
||||
previous = btn;
|
||||
}
|
||||
// 最后一个按钮贴右侧
|
||||
[previous mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.leftContainer.mas_right);
|
||||
}];
|
||||
self.leftButtonsInternal = buttons.copy;
|
||||
|
||||
// 初始刷新地球键的可见性与事件绑定
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
- (UIButton *)buildActionButtonAtIndex:(NSInteger)idx {
|
||||
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
btn.layer.cornerRadius = 16;
|
||||
btn.layer.masksToBounds = YES;
|
||||
btn.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
btn.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||
NSString *title = (idx < self.leftButtonTitles.count) ? self.leftButtonTitles[idx] : [NSString stringWithFormat:@"Item%ld", (long)(idx+1)];
|
||||
[btn setTitle:title forState:UIControlStateNormal];
|
||||
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
btn.tag = idx;
|
||||
[btn addTarget:self action:@selector(onLeftAction:) forControlEvents:UIControlEventTouchUpInside];
|
||||
return btn;
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)onLeftAction:(UIButton *)sender {
|
||||
if ([self.delegate respondsToSelector:@selector(toolBar:didTapActionAtIndex:)]) {
|
||||
[self.delegate toolBar:self didTapActionAtIndex:sender.tag];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onSettings {
|
||||
if ([self.delegate respondsToSelector:@selector(toolBarDidTapSettings:)]) {
|
||||
[self.delegate toolBarDidTapSettings:self];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIView *)leftContainer {
|
||||
if (!_leftContainer) {
|
||||
_leftContainer = [[UIView alloc] init];
|
||||
_leftContainer.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
return _leftContainer;
|
||||
}
|
||||
|
||||
- (UIButton *)settingsButtonInternal {
|
||||
if (!_settingsButtonInternal) {
|
||||
_settingsButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_settingsButtonInternal.layer.cornerRadius = 16;
|
||||
_settingsButtonInternal.layer.masksToBounds = YES;
|
||||
_settingsButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
[_settingsButtonInternal setTitle:@"⚙︎" forState:UIControlStateNormal]; // 简单的齿轮符号
|
||||
[_settingsButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
[_settingsButtonInternal addTarget:self action:@selector(onSettings) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _settingsButtonInternal;
|
||||
}
|
||||
|
||||
- (UIButton *)globeButtonInternal {
|
||||
if (!_globeButtonInternal) {
|
||||
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_globeButtonInternal.layer.cornerRadius = 16;
|
||||
_globeButtonInternal.layer.masksToBounds = YES;
|
||||
_globeButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
[_globeButtonInternal setTitle:@"🌐" forState:UIControlStateNormal];
|
||||
[_globeButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
}
|
||||
return _globeButtonInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Globe (Input Mode Switch)
|
||||
|
||||
// 根据宿主是否已提供系统切换键,决定是否显示地球按钮;并绑定系统事件。
|
||||
- (void)kb_refreshGlobeVisibility {
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
BOOL needSwitchKey = YES;
|
||||
if (ivc && [ivc respondsToSelector:@selector(needsInputModeSwitchKey)]) {
|
||||
needSwitchKey = ivc.needsInputModeSwitchKey; // YES 表示自定义键盘需要提供切换键
|
||||
}
|
||||
|
||||
self.globeButtonInternal.hidden = !needSwitchKey;
|
||||
|
||||
// 重新调整 leftContainer 的左约束:若不需要地球键,则贴左边距 12
|
||||
[self.leftContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
if (needSwitchKey) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
} else {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
}
|
||||
make.right.equalTo(self.settingsButtonInternal.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 绑定系统提供的输入法切换处理(点按切换、长按弹出列表)
|
||||
// 仅在需要时绑定,避免多余的事件转发
|
||||
[self.globeButtonInternal removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
|
||||
if (needSwitchKey && ivc) {
|
||||
SEL sel = NSSelectorFromString(@"handleInputModeListFromView:withEvent:");
|
||||
if ([ivc respondsToSelector:sel]) {
|
||||
[self.globeButtonInternal addTarget:ivc action:sel forControlEvents:UIControlEventAllTouchEvents];
|
||||
} else {
|
||||
// 回退:至少在点按时切换
|
||||
[self.globeButtonInternal addTarget:ivc action:@selector(advanceToNextInputMode) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
30
Podfile
30
Podfile
@@ -2,23 +2,33 @@
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
platform :ios, '13.0'
|
||||
|
||||
target 'CustomKeyboard' do
|
||||
# Comment the next line if you don't want to use dynamic frameworks
|
||||
use_frameworks!
|
||||
|
||||
# Pods for CustomKeyboard
|
||||
|
||||
end
|
||||
|
||||
target 'keyBoard' do
|
||||
# Comment the next line if you don't want to use dynamic frameworks
|
||||
use_frameworks!
|
||||
|
||||
pod 'AFNetworking','4.0.1'
|
||||
pod 'Bugly','2.6.1'
|
||||
pod 'Bugly', :configurations => ['Release']
|
||||
pod 'Masonry', '1.1.0'
|
||||
pod 'MBProgressHUD', '1.2.0'
|
||||
pod 'MJExtension', '3.4.2'
|
||||
pod 'MJRefresh', '3.7.9'
|
||||
pod 'SDWebImage', '5.21.1'
|
||||
|
||||
pod 'DZNEmptyDataSet', '1.8.1'
|
||||
pod 'HWPanModal', '~> 0.9.9'
|
||||
pod 'FLAnimatedImage', '~> 1.0.17'
|
||||
pod 'LookinServer', :configurations => ['Debug']
|
||||
end
|
||||
|
||||
target 'CustomKeyboard' do
|
||||
# Comment the next line if you don't want to use dynamic frameworks
|
||||
use_frameworks!
|
||||
|
||||
pod 'AFNetworking','4.0.1'
|
||||
|
||||
pod 'Masonry', '1.1.0'
|
||||
pod 'MBProgressHUD', '1.2.0'
|
||||
pod 'MJExtension', '3.4.2'
|
||||
pod 'DZNEmptyDataSet', '1.8.1'
|
||||
end
|
||||
|
||||
|
||||
|
||||
26
Podfile.lock
26
Podfile.lock
@@ -15,7 +15,14 @@ PODS:
|
||||
- AFNetworking/UIKit (4.0.1):
|
||||
- AFNetworking/NSURLSession
|
||||
- Bugly (2.6.1)
|
||||
- DZNEmptyDataSet (1.8.1)
|
||||
- FLAnimatedImage (1.0.17)
|
||||
- HWPanModal (0.9.9)
|
||||
- LookinServer (1.2.8):
|
||||
- LookinServer/Core (= 1.2.8)
|
||||
- LookinServer/Core (1.2.8)
|
||||
- Masonry (1.1.0)
|
||||
- MBProgressHUD (1.2.0)
|
||||
- MJExtension (3.4.2)
|
||||
- MJRefresh (3.7.9)
|
||||
- SDWebImage (5.21.1):
|
||||
@@ -24,8 +31,13 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- AFNetworking (= 4.0.1)
|
||||
- Bugly (= 2.6.1)
|
||||
- Bugly
|
||||
- DZNEmptyDataSet (= 1.8.1)
|
||||
- FLAnimatedImage (~> 1.0.17)
|
||||
- HWPanModal (~> 0.9.9)
|
||||
- LookinServer
|
||||
- Masonry (= 1.1.0)
|
||||
- MBProgressHUD (= 1.2.0)
|
||||
- MJExtension (= 3.4.2)
|
||||
- MJRefresh (= 3.7.9)
|
||||
- SDWebImage (= 5.21.1)
|
||||
@@ -34,7 +46,12 @@ SPEC REPOS:
|
||||
https://github.com/CocoaPods/Specs.git:
|
||||
- AFNetworking
|
||||
- Bugly
|
||||
- DZNEmptyDataSet
|
||||
- FLAnimatedImage
|
||||
- HWPanModal
|
||||
- LookinServer
|
||||
- Masonry
|
||||
- MBProgressHUD
|
||||
- MJExtension
|
||||
- MJRefresh
|
||||
- SDWebImage
|
||||
@@ -42,11 +59,16 @@ SPEC REPOS:
|
||||
SPEC CHECKSUMS:
|
||||
AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58
|
||||
Bugly: 217ac2ce5f0f2626d43dbaa4f70764c953a26a31
|
||||
DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7
|
||||
FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b
|
||||
HWPanModal: b57a6717d3cdcd666bff44f9dd2a5be9f4d6f5d2
|
||||
LookinServer: 1b2b61c6402ae29fa22182d48f5cd067b4e99e80
|
||||
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
|
||||
MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406
|
||||
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
|
||||
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
|
||||
PODFILE CHECKSUM: b3c72fe500149c35040cdd73c1d91fe05777bc5f
|
||||
PODFILE CHECKSUM: c407e365492f78edcfea5c78b0fb36d8bf0e447d
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
9
Pods/DZNEmptyDataSet/LICENSE
generated
Normal file
9
Pods/DZNEmptyDataSet/LICENSE
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Ignacio Romero Zurbuchen iromero@dzen.cl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
296
Pods/DZNEmptyDataSet/README.md
generated
Normal file
296
Pods/DZNEmptyDataSet/README.md
generated
Normal file
@@ -0,0 +1,296 @@
|
||||
DZNEmptyDataSet
|
||||
=================
|
||||
|
||||
[](http://cocoadocs.org/docsets/DZNEmptyDataSet/)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](http://opensource.org/licenses/MIT)
|
||||
|
||||
### Projects using this library
|
||||
|
||||
[Add your project to the list here](https://github.com/dzenbot/DZNEmptyDataSet/wiki/Projects-using-DZNEmptyDataSet) and provide a (320px wide) render of the result.
|
||||
|
||||
|
||||
### The Empty Data Set Pattern
|
||||
Also known as *[Empty State](http://emptystat.es/)* or *[Blank Slate](http://patternry.com/p=blank-slate/)*.
|
||||
|
||||
Most applications show lists of content (data sets), which many turn out to be empty at one point, specially for new users with blank accounts. Empty screens create confusion by not being clear about what's going on, if there is an error/bug or if the user is supposed to do something within your app to be able to consume the content.
|
||||
|
||||
Please read this very interesting article about [*Designing For The Empty States*](http://tympanus.net/codrops/2013/01/09/designing-for-the-empty-states/).
|
||||
|
||||

|
||||

|
||||
(*These are real life examples, available in the 'Applications' sample project*)
|
||||
|
||||
**[Empty Data Sets](http://pttrns.com/?did=1&scid=30)** are helpful for:
|
||||
* Avoiding white-screens and communicating to your users why the screen is empty.
|
||||
* Calling to action (particularly as an onboarding process).
|
||||
* Avoiding other interruptive mechanisms like showing error alerts.
|
||||
* Being consistent and improving the user experience.
|
||||
* Delivering a brand presence.
|
||||
|
||||
|
||||
### Features
|
||||
* Compatible with UITableView and UICollectionView. Also compatible with UISearchDisplayController and UIScrollView.
|
||||
* Gives multiple possibilities of layout and appearance, by showing an image and/or title label and/or description label and/or button.
|
||||
* Uses NSAttributedString for easier appearance customisation.
|
||||
* Uses auto-layout to automagically center the content to the tableview, with auto-rotation support. Also accepts custom vertical and horizontal alignment.
|
||||
* Background color customisation.
|
||||
* Allows tap gesture on the whole tableview rectangle (useful for resigning first responder or similar actions).
|
||||
* For more advanced customisation, it allows a custom view.
|
||||
* Compatible with Storyboard.
|
||||
* Compatible with iOS 6 or later.
|
||||
* Compatible with iPhone and iPad.
|
||||
* **App Store ready**
|
||||
|
||||
This library has been designed in a way where you won't need to extend UITableView or UICollectionView class. It will still work when using UITableViewController or UICollectionViewController.
|
||||
By just conforming to DZNEmptyDataSetSource & DZNEmptyDataSetDelegate, you will be able to fully customize the content and appearance of the empty states for your application.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Available in [CocoaPods](http://cocoapods.org/?q=DZNEmptyDataSet)
|
||||
```ruby
|
||||
pod 'DZNEmptyDataSet'
|
||||
```
|
||||
|
||||
To integrate DZNEmptyDataSet into your Xcode project using Carthage, specify it in your `Cartfile`:
|
||||
|
||||
```ruby
|
||||
github "dzenbot/DZNEmptyDataSet"
|
||||
```
|
||||
|
||||
|
||||
## How to use
|
||||
For complete documentation, [visit CocoaPods' auto-generated doc](http://cocoadocs.org/docsets/DZNEmptyDataSet/)
|
||||
|
||||
### Import
|
||||
```objc
|
||||
#import "UIScrollView+EmptyDataSet.h"
|
||||
```
|
||||
Unless you are importing as a framework, then do:
|
||||
```objc
|
||||
#import "<DZNEmptyDataSet/UIScrollView+EmptyDataSet.h>"
|
||||
```
|
||||
|
||||
### Protocol Conformance
|
||||
Conform to datasource and/or delegate.
|
||||
```objc
|
||||
@interface MainViewController : UITableViewController <DZNEmptyDataSetSource, DZNEmptyDataSetDelegate>
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.tableView.emptyDataSetSource = self;
|
||||
self.tableView.emptyDataSetDelegate = self;
|
||||
|
||||
// A little trick for removing the cell separators
|
||||
self.tableView.tableFooterView = [UIView new];
|
||||
}
|
||||
```
|
||||
|
||||
### Data Source Implementation
|
||||
Return the content you want to show on the empty state, and take advantage of NSAttributedString features to customise the text appearance.
|
||||
|
||||
The image for the empty state:
|
||||
```objc
|
||||
- (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView
|
||||
{
|
||||
return [UIImage imageNamed:@"empty_placeholder"];
|
||||
}
|
||||
```
|
||||
|
||||
The image view animation
|
||||
```objc
|
||||
- (CAAnimation *)imageAnimationForEmptyDataSet:(UIScrollView *)scrollView
|
||||
{
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath: @"transform"];
|
||||
|
||||
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
|
||||
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI_2, 0.0, 0.0, 1.0)];
|
||||
|
||||
animation.duration = 0.25;
|
||||
animation.cumulative = YES;
|
||||
animation.repeatCount = MAXFLOAT;
|
||||
|
||||
return animation;
|
||||
}
|
||||
```
|
||||
|
||||
The attributed string for the title of the empty state:
|
||||
```objc
|
||||
- (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView
|
||||
{
|
||||
NSString *text = @"Please Allow Photo Access";
|
||||
|
||||
NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:18.0f],
|
||||
NSForegroundColorAttributeName: [UIColor darkGrayColor]};
|
||||
|
||||
return [[NSAttributedString alloc] initWithString:text attributes:attributes];
|
||||
}
|
||||
```
|
||||
|
||||
The attributed string for the description of the empty state:
|
||||
```objc
|
||||
- (NSAttributedString *)descriptionForEmptyDataSet:(UIScrollView *)scrollView
|
||||
{
|
||||
NSString *text = @"This allows you to share photos from your library and save photos to your camera roll.";
|
||||
|
||||
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
|
||||
paragraph.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
paragraph.alignment = NSTextAlignmentCenter;
|
||||
|
||||
NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:14.0f],
|
||||
NSForegroundColorAttributeName: [UIColor lightGrayColor],
|
||||
NSParagraphStyleAttributeName: paragraph};
|
||||
|
||||
return [[NSAttributedString alloc] initWithString:text attributes:attributes];
|
||||
}
|
||||
```
|
||||
|
||||
The attributed string to be used for the specified button state:
|
||||
```objc
|
||||
- (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state
|
||||
{
|
||||
NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:17.0f]};
|
||||
|
||||
return [[NSAttributedString alloc] initWithString:@"Continue" attributes:attributes];
|
||||
}
|
||||
```
|
||||
|
||||
or the image to be used for the specified button state:
|
||||
```objc
|
||||
- (UIImage *)buttonImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state
|
||||
{
|
||||
return [UIImage imageNamed:@"button_image"];
|
||||
}
|
||||
```
|
||||
|
||||
The background color for the empty state:
|
||||
```objc
|
||||
- (UIColor *)backgroundColorForEmptyDataSet:(UIScrollView *)scrollView
|
||||
{
|
||||
return [UIColor whiteColor];
|
||||
}
|
||||
```
|
||||
|
||||
If you need a more complex layout, you can return a custom view instead:
|
||||
```objc
|
||||
- (UIView *)customViewForEmptyDataSet:(UIScrollView *)scrollView
|
||||
{
|
||||
UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
|
||||
[activityView startAnimating];
|
||||
return activityView;
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, you can also adjust the vertical alignment of the content view (ie: useful when there is tableHeaderView visible):
|
||||
```objc
|
||||
- (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView
|
||||
{
|
||||
return -self.tableView.tableHeaderView.frame.size.height/2.0f;
|
||||
}
|
||||
```
|
||||
|
||||
Finally, you can separate components from each other (default separation is 11 pts):
|
||||
```objc
|
||||
- (CGFloat)spaceHeightForEmptyDataSet:(UIScrollView *)scrollView
|
||||
{
|
||||
return 20.0f;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Delegate Implementation
|
||||
Return the behaviours you would expect from the empty states, and receive the user events.
|
||||
|
||||
Asks to know if the empty state should be rendered and displayed (Default is YES) :
|
||||
```objc
|
||||
- (BOOL)emptyDataSetShouldDisplay:(UIScrollView *)scrollView
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
```
|
||||
|
||||
Asks for interaction permission (Default is YES) :
|
||||
```objc
|
||||
- (BOOL)emptyDataSetShouldAllowTouch:(UIScrollView *)scrollView
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
```
|
||||
|
||||
Asks for scrolling permission (Default is NO) :
|
||||
```objc
|
||||
- (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
```
|
||||
|
||||
Asks for image view animation permission (Default is NO) :
|
||||
```objc
|
||||
- (BOOL) emptyDataSetShouldAllowImageViewAnimate:(UIScrollView *)scrollView
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
```
|
||||
|
||||
Notifies when the dataset view was tapped:
|
||||
```objc
|
||||
- (void)emptyDataSet:(UIScrollView *)scrollView didTapView:(UIView *)view
|
||||
{
|
||||
// Do something
|
||||
}
|
||||
```
|
||||
|
||||
Notifies when the data set call to action button was tapped:
|
||||
```objc
|
||||
- (void)emptyDataSet:(UIScrollView *)scrollView didTapButton:(UIButton *)button
|
||||
{
|
||||
// Do something
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh layout
|
||||
If you need to refresh the empty state layout, simply call:
|
||||
|
||||
```objc
|
||||
[self.tableView reloadData];
|
||||
```
|
||||
or
|
||||
```objc
|
||||
[self.collectionView reloadData];
|
||||
```
|
||||
depending of which you are using.
|
||||
|
||||
### Force layout update
|
||||
You can also call `[self.tableView reloadEmptyDataSet]` to invalidate the current empty state layout and trigger a layout update, bypassing `-reloadData`. This might be useful if you have a lot of logic on your data source that you want to avoid calling, when not needed. `[self.scrollView reloadEmptyDataSet]` is the only way to refresh content when using with UIScrollView.
|
||||
|
||||
|
||||
## Sample projects
|
||||
|
||||
#### Applications
|
||||
This project replicates several popular application's empty states (~20) with their exact content and appearance, such as Airbnb, Dropbox, Facebook, Foursquare, and many others. See how easy and flexible it is to customize the appearance of your empty states. You can also use this project as a playground to test things.
|
||||
|
||||
#### Countries
|
||||
This project shows a list of the world countries loaded from CoreData. It uses NSFecthedResultController for filtering search. When searching and no content is matched, a simple empty state is shown. See how to interact between the UITableViewDataSource and the DZNEmptyDataSetSource protocols, while using a typical CoreData stack.
|
||||
|
||||
#### Colors
|
||||
This project is a simple example of how this library also works with UICollectionView and UISearchDisplayController, while using Storyboards.
|
||||
|
||||
|
||||
## Collaboration
|
||||
Feel free to collaborate with ideas, issues and/or pull requests.
|
||||
|
||||
|
||||
## License
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2016 Ignacio Romero Zurbuchen iromero@dzen.cl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
280
Pods/DZNEmptyDataSet/Source/UIScrollView+EmptyDataSet.h
generated
Normal file
280
Pods/DZNEmptyDataSet/Source/UIScrollView+EmptyDataSet.h
generated
Normal file
@@ -0,0 +1,280 @@
|
||||
//
|
||||
// UIScrollView+EmptyDataSet.h
|
||||
// DZNEmptyDataSet
|
||||
// https://github.com/dzenbot/DZNEmptyDataSet
|
||||
//
|
||||
// Created by Ignacio Romero Zurbuchen on 6/20/14.
|
||||
// Copyright (c) 2016 DZN Labs. All rights reserved.
|
||||
// Licence: MIT-Licence
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol DZNEmptyDataSetSource;
|
||||
@protocol DZNEmptyDataSetDelegate;
|
||||
|
||||
#define DZNEmptyDataSetDeprecated(instead) DEPRECATED_MSG_ATTRIBUTE(" Use " # instead " instead")
|
||||
|
||||
/**
|
||||
A drop-in UITableView/UICollectionView superclass category for showing empty datasets whenever the view has no content to display.
|
||||
@discussion It will work automatically, by just conforming to DZNEmptyDataSetSource, and returning the data you want to show.
|
||||
*/
|
||||
@interface UIScrollView (EmptyDataSet)
|
||||
|
||||
/** The empty datasets data source. */
|
||||
@property (nonatomic, weak) IBOutlet id <DZNEmptyDataSetSource> emptyDataSetSource;
|
||||
/** The empty datasets delegate. */
|
||||
@property (nonatomic, weak) IBOutlet id <DZNEmptyDataSetDelegate> emptyDataSetDelegate;
|
||||
/** YES if any empty dataset is visible. */
|
||||
@property (nonatomic, readonly, getter = isEmptyDataSetVisible) BOOL emptyDataSetVisible;
|
||||
|
||||
/**
|
||||
Reloads the empty dataset content receiver.
|
||||
@discussion Call this method to force all the data to refresh. Calling -reloadData is similar, but this forces only the empty dataset to reload, not the entire table view or collection view.
|
||||
*/
|
||||
- (void)reloadEmptyDataSet;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/**
|
||||
The object that acts as the data source of the empty datasets.
|
||||
@discussion The data source must adopt the DZNEmptyDataSetSource protocol. The data source is not retained. All data source methods are optional.
|
||||
*/
|
||||
@protocol DZNEmptyDataSetSource <NSObject>
|
||||
@optional
|
||||
|
||||
/**
|
||||
Asks the data source for the title of the dataset.
|
||||
The dataset uses a fixed font style by default, if no attributes are set. If you want a different font style, return a attributed string.
|
||||
|
||||
@param scrollView A scrollView subclass informing the data source.
|
||||
@return An attributed string for the dataset title, combining font, text color, text pararaph style, etc.
|
||||
*/
|
||||
- (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the data source for the description of the dataset.
|
||||
The dataset uses a fixed font style by default, if no attributes are set. If you want a different font style, return a attributed string.
|
||||
|
||||
@param scrollView A scrollView subclass informing the data source.
|
||||
@return An attributed string for the dataset description text, combining font, text color, text pararaph style, etc.
|
||||
*/
|
||||
- (NSAttributedString *)descriptionForEmptyDataSet:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the data source for the image of the dataset.
|
||||
|
||||
@param scrollView A scrollView subclass informing the data source.
|
||||
@return An image for the dataset.
|
||||
*/
|
||||
- (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView;
|
||||
|
||||
|
||||
/**
|
||||
Asks the data source for a tint color of the image dataset. Default is nil.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the data source.
|
||||
@return A color to tint the image of the dataset.
|
||||
*/
|
||||
- (UIColor *)imageTintColorForEmptyDataSet:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
* Asks the data source for the image animation of the dataset.
|
||||
*
|
||||
* @param scrollView A scrollView subclass object informing the delegate.
|
||||
*
|
||||
* @return image animation
|
||||
*/
|
||||
- (CAAnimation *) imageAnimationForEmptyDataSet:(UIScrollView *) scrollView;
|
||||
|
||||
/**
|
||||
Asks the data source for the title to be used for the specified button state.
|
||||
The dataset uses a fixed font style by default, if no attributes are set. If you want a different font style, return a attributed string.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the data source.
|
||||
@param state The state that uses the specified title. The possible values are described in UIControlState.
|
||||
@return An attributed string for the dataset button title, combining font, text color, text pararaph style, etc.
|
||||
*/
|
||||
- (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state;
|
||||
|
||||
/**
|
||||
Asks the data source for the image to be used for the specified button state.
|
||||
This method will override buttonTitleForEmptyDataSet:forState: and present the image only without any text.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the data source.
|
||||
@param state The state that uses the specified title. The possible values are described in UIControlState.
|
||||
@return An image for the dataset button imageview.
|
||||
*/
|
||||
- (UIImage *)buttonImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state;
|
||||
|
||||
/**
|
||||
Asks the data source for a background image to be used for the specified button state.
|
||||
There is no default style for this call.
|
||||
|
||||
@param scrollView A scrollView subclass informing the data source.
|
||||
@param state The state that uses the specified image. The values are described in UIControlState.
|
||||
@return An attributed string for the dataset button title, combining font, text color, text pararaph style, etc.
|
||||
*/
|
||||
- (UIImage *)buttonBackgroundImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state;
|
||||
|
||||
/**
|
||||
Asks the data source for the background color of the dataset. Default is clear color.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the data source.
|
||||
@return A color to be applied to the dataset background view.
|
||||
*/
|
||||
- (UIColor *)backgroundColorForEmptyDataSet:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the data source for a custom view to be displayed instead of the default views such as labels, imageview and button. Default is nil.
|
||||
Use this method to show an activity view indicator for loading feedback, or for complete custom empty data set.
|
||||
Returning a custom view will ignore -offsetForEmptyDataSet and -spaceHeightForEmptyDataSet configurations.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the delegate.
|
||||
@return The custom view.
|
||||
*/
|
||||
- (UIView *)customViewForEmptyDataSet:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the data source for a offset for vertical and horizontal alignment of the content. Default is CGPointZero.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the delegate.
|
||||
@return The offset for vertical and horizontal alignment.
|
||||
*/
|
||||
- (CGPoint)offsetForEmptyDataSet:(UIScrollView *)scrollView DZNEmptyDataSetDeprecated(-verticalOffsetForEmptyDataSet:);
|
||||
- (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the data source for a vertical space between elements. Default is 11 pts.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the delegate.
|
||||
@return The space height between elements.
|
||||
*/
|
||||
- (CGFloat)spaceHeightForEmptyDataSet:(UIScrollView *)scrollView;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/**
|
||||
The object that acts as the delegate of the empty datasets.
|
||||
@discussion The delegate can adopt the DZNEmptyDataSetDelegate protocol. The delegate is not retained. All delegate methods are optional.
|
||||
|
||||
@discussion All delegate methods are optional. Use this delegate for receiving action callbacks.
|
||||
*/
|
||||
@protocol DZNEmptyDataSetDelegate <NSObject>
|
||||
@optional
|
||||
|
||||
/**
|
||||
Asks the delegate to know if the empty dataset should fade in when displayed. Default is YES.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the delegate.
|
||||
@return YES if the empty dataset should fade in.
|
||||
*/
|
||||
- (BOOL)emptyDataSetShouldFadeIn:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the delegate to know if the empty dataset should still be displayed when the amount of items is more than 0. Default is NO
|
||||
|
||||
@param scrollView A scrollView subclass object informing the delegate.
|
||||
@return YES if empty dataset should be forced to display
|
||||
*/
|
||||
- (BOOL)emptyDataSetShouldBeForcedToDisplay:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the delegate to know if the empty dataset should be rendered and displayed. Default is YES.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the delegate.
|
||||
@return YES if the empty dataset should show.
|
||||
*/
|
||||
- (BOOL)emptyDataSetShouldDisplay:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the delegate for touch permission. Default is YES.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the delegate.
|
||||
@return YES if the empty dataset receives touch gestures.
|
||||
*/
|
||||
- (BOOL)emptyDataSetShouldAllowTouch:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the delegate for scroll permission. Default is NO.
|
||||
|
||||
@param scrollView A scrollView subclass object informing the delegate.
|
||||
@return YES if the empty dataset is allowed to be scrollable.
|
||||
*/
|
||||
- (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Asks the delegate for image view animation permission. Default is NO.
|
||||
Make sure to return a valid CAAnimation object from imageAnimationForEmptyDataSet:
|
||||
|
||||
@param scrollView A scrollView subclass object informing the delegate.
|
||||
@return YES if the empty dataset is allowed to animate
|
||||
*/
|
||||
- (BOOL)emptyDataSetShouldAnimateImageView:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Tells the delegate that the empty dataset view was tapped.
|
||||
Use this method either to resignFirstResponder of a textfield or searchBar.
|
||||
|
||||
@param scrollView A scrollView subclass informing the delegate.
|
||||
*/
|
||||
- (void)emptyDataSetDidTapView:(UIScrollView *)scrollView DZNEmptyDataSetDeprecated(-emptyDataSet:didTapView:);
|
||||
|
||||
/**
|
||||
Tells the delegate that the action button was tapped.
|
||||
|
||||
@param scrollView A scrollView subclass informing the delegate.
|
||||
*/
|
||||
- (void)emptyDataSetDidTapButton:(UIScrollView *)scrollView DZNEmptyDataSetDeprecated(-emptyDataSet:didTapButton:);
|
||||
|
||||
/**
|
||||
Tells the delegate that the empty dataset view was tapped.
|
||||
Use this method either to resignFirstResponder of a textfield or searchBar.
|
||||
|
||||
@param scrollView A scrollView subclass informing the delegate.
|
||||
@param view the view tapped by the user
|
||||
*/
|
||||
- (void)emptyDataSet:(UIScrollView *)scrollView didTapView:(UIView *)view;
|
||||
|
||||
/**
|
||||
Tells the delegate that the action button was tapped.
|
||||
|
||||
@param scrollView A scrollView subclass informing the delegate.
|
||||
@param button the button tapped by the user
|
||||
*/
|
||||
- (void)emptyDataSet:(UIScrollView *)scrollView didTapButton:(UIButton *)button;
|
||||
|
||||
/**
|
||||
Tells the delegate that the empty data set will appear.
|
||||
|
||||
@param scrollView A scrollView subclass informing the delegate.
|
||||
*/
|
||||
- (void)emptyDataSetWillAppear:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Tells the delegate that the empty data set did appear.
|
||||
|
||||
@param scrollView A scrollView subclass informing the delegate.
|
||||
*/
|
||||
- (void)emptyDataSetDidAppear:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Tells the delegate that the empty data set will disappear.
|
||||
|
||||
@param scrollView A scrollView subclass informing the delegate.
|
||||
*/
|
||||
- (void)emptyDataSetWillDisappear:(UIScrollView *)scrollView;
|
||||
|
||||
/**
|
||||
Tells the delegate that the empty data set did disappear.
|
||||
|
||||
@param scrollView A scrollView subclass informing the delegate.
|
||||
*/
|
||||
- (void)emptyDataSetDidDisappear:(UIScrollView *)scrollView;
|
||||
|
||||
@end
|
||||
|
||||
#undef DZNEmptyDataSetDeprecated
|
||||
|
||||
1074
Pods/DZNEmptyDataSet/Source/UIScrollView+EmptyDataSet.m
generated
Normal file
1074
Pods/DZNEmptyDataSet/Source/UIScrollView+EmptyDataSet.m
generated
Normal file
File diff suppressed because it is too large
Load Diff
820
Pods/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.m
generated
Executable file
820
Pods/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.m
generated
Executable file
@@ -0,0 +1,820 @@
|
||||
//
|
||||
// FLAnimatedImage.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "FLAnimatedImage.h"
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#if __has_include(<MobileCoreServices/MobileCoreServices.h>)
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#else
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#endif
|
||||
|
||||
|
||||
// From vm_param.h, define for iOS 8.0 or higher to build on device.
|
||||
#ifndef BYTE_SIZE
|
||||
#define BYTE_SIZE 8 // byte size in bits
|
||||
#endif
|
||||
|
||||
#define MEGABYTE (1024 * 1024)
|
||||
|
||||
// This is how the fastest browsers do it as per 2012: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility
|
||||
const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum = 0.02;
|
||||
|
||||
// An animated image's data size (dimensions * frameCount) category; its value is the max allowed memory (in MB).
|
||||
// E.g.: A 100x200px GIF with 30 frames is ~2.3MB in our pixel format and would fall into the `FLAnimatedImageDataSizeCategoryAll` category.
|
||||
typedef NS_ENUM(NSUInteger, FLAnimatedImageDataSizeCategory) {
|
||||
FLAnimatedImageDataSizeCategoryAll = 10, // All frames permanently in memory (be nice to the CPU)
|
||||
FLAnimatedImageDataSizeCategoryDefault = 75, // A frame cache of default size in memory (usually real-time performance and keeping low memory profile)
|
||||
FLAnimatedImageDataSizeCategoryOnDemand = 250, // Only keep one frame at the time in memory (easier on memory, slowest performance)
|
||||
FLAnimatedImageDataSizeCategoryUnsupported // Even for one frame too large, computer says no.
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLAnimatedImageFrameCacheSize) {
|
||||
FLAnimatedImageFrameCacheSizeNoLimit = 0, // 0 means no specific limit
|
||||
FLAnimatedImageFrameCacheSizeLowMemory = 1, // The minimum frame cache size; this will produce frames on-demand.
|
||||
FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning = 2, // If we can produce the frames faster than we consume, one frame ahead will already result in a stutter-free playback.
|
||||
FLAnimatedImageFrameCacheSizeDefault = 5 // Build up a comfy buffer window to cope with CPU hiccups etc.
|
||||
};
|
||||
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@protocol FLAnimatedImageDebugDelegate <NSObject>
|
||||
@optional
|
||||
- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didUpdateCachedFrames:(NSIndexSet *)indexesOfFramesInCache;
|
||||
- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didRequestCachedFrame:(NSUInteger)index;
|
||||
- (CGFloat)debug_animatedImagePredrawingSlowdownFactor:(FLAnimatedImage *)animatedImage;
|
||||
@end
|
||||
#endif
|
||||
|
||||
|
||||
@interface FLAnimatedImage ()
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeOptimal; // The optimal number of frames to cache based on image size & number of frames; never changes
|
||||
@property (nonatomic, assign, readonly, getter=isPredrawingEnabled) BOOL predrawingEnabled; // Enables predrawing of images to improve performance.
|
||||
@property (nonatomic, assign) NSUInteger frameCacheSizeMaxInternal; // Allow to cap the cache size e.g. when memory warnings occur; 0 means no specific limit (default)
|
||||
@property (nonatomic, assign) NSUInteger requestedFrameIndex; // Most recently requested frame index
|
||||
@property (nonatomic, assign, readonly) NSUInteger posterImageFrameIndex; // Index of non-purgable poster image; never changes
|
||||
@property (nonatomic, strong, readonly) NSMutableDictionary *cachedFramesForIndexes;
|
||||
@property (nonatomic, strong, readonly) NSMutableIndexSet *cachedFrameIndexes; // Indexes of cached frames
|
||||
@property (nonatomic, strong, readonly) NSMutableIndexSet *requestedFrameIndexes; // Indexes of frames that are currently produced in the background
|
||||
@property (nonatomic, strong, readonly) NSIndexSet *allFramesIndexSet; // Default index set with the full range of indexes; never changes
|
||||
@property (nonatomic, assign) NSUInteger memoryWarningCount;
|
||||
@property (nonatomic, strong, readonly) dispatch_queue_t serialQueue;
|
||||
@property (nonatomic, strong, readonly) __attribute__((NSObject)) CGImageSourceRef imageSource;
|
||||
|
||||
// The weak proxy is used to break retain cycles with delayed actions from memory warnings.
|
||||
// We are lying about the actual type here to gain static type checking and eliminate casts.
|
||||
// The actual type of the object is `FLWeakProxy`.
|
||||
@property (nonatomic, strong, readonly) FLAnimatedImage *weakProxy;
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@property (nonatomic, weak) id<FLAnimatedImageDebugDelegate> debug_delegate;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// For custom dispatching of memory warnings to avoid deallocation races since NSNotificationCenter doesn't retain objects it is notifying.
|
||||
static NSHashTable *allAnimatedImagesWeak;
|
||||
|
||||
@implementation FLAnimatedImage
|
||||
|
||||
#pragma mark - Accessors
|
||||
#pragma mark Public
|
||||
|
||||
// This is the definite value the frame cache needs to size itself to.
|
||||
- (NSUInteger)frameCacheSizeCurrent
|
||||
{
|
||||
NSUInteger frameCacheSizeCurrent = self.frameCacheSizeOptimal;
|
||||
|
||||
// If set, respect the caps.
|
||||
if (self.frameCacheSizeMax > FLAnimatedImageFrameCacheSizeNoLimit) {
|
||||
frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMax);
|
||||
}
|
||||
|
||||
if (self.frameCacheSizeMaxInternal > FLAnimatedImageFrameCacheSizeNoLimit) {
|
||||
frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMaxInternal);
|
||||
}
|
||||
|
||||
return frameCacheSizeCurrent;
|
||||
}
|
||||
|
||||
|
||||
- (void)setFrameCacheSizeMax:(NSUInteger)frameCacheSizeMax
|
||||
{
|
||||
if (_frameCacheSizeMax != frameCacheSizeMax) {
|
||||
|
||||
// Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
|
||||
const BOOL willFrameCacheSizeShrink = (frameCacheSizeMax < self.frameCacheSizeCurrent);
|
||||
|
||||
// Update the value
|
||||
_frameCacheSizeMax = frameCacheSizeMax;
|
||||
|
||||
if (willFrameCacheSizeShrink) {
|
||||
[self purgeFrameCacheIfNeeded];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
- (void)setFrameCacheSizeMaxInternal:(NSUInteger)frameCacheSizeMaxInternal
|
||||
{
|
||||
if (_frameCacheSizeMaxInternal != frameCacheSizeMaxInternal) {
|
||||
|
||||
// Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
|
||||
BOOL willFrameCacheSizeShrink = (frameCacheSizeMaxInternal < self.frameCacheSizeCurrent);
|
||||
|
||||
// Update the value
|
||||
_frameCacheSizeMaxInternal = frameCacheSizeMaxInternal;
|
||||
|
||||
if (willFrameCacheSizeShrink) {
|
||||
[self purgeFrameCacheIfNeeded];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Life Cycle
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
if (self == [FLAnimatedImage class]) {
|
||||
// UIKit memory warning notification handler shared by all of the instances
|
||||
allAnimatedImagesWeak = [NSHashTable weakObjectsHashTable];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
|
||||
// UIKit notifications are posted on the main thread. didReceiveMemoryWarning: is expecting the main run loop, and we don't lock on allAnimatedImagesWeak
|
||||
NSAssert([NSThread isMainThread], @"Received memory warning on non-main thread");
|
||||
// Get a strong reference to all of the images. If an instance is returned in this array, it is still live and has not entered dealloc.
|
||||
// Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
|
||||
NSArray *images = nil;
|
||||
@synchronized(allAnimatedImagesWeak) {
|
||||
images = [[allAnimatedImagesWeak allObjects] copy];
|
||||
}
|
||||
// Now issue notifications to all of the images while holding a strong reference to them
|
||||
[images makeObjectsPerformSelector:@selector(didReceiveMemoryWarning:) withObject:note];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
FLAnimatedImage *_Nullable const animatedImage = [self initWithAnimatedGIFData:nil];
|
||||
if (!animatedImage) {
|
||||
FLLog(FLLogLevelError, @"Use `-initWithAnimatedGIFData:` and supply the animated GIF data as an argument to initialize an object of type `FLAnimatedImage`.");
|
||||
}
|
||||
return animatedImage;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data
|
||||
{
|
||||
return [self initWithAnimatedGIFData:data optimalFrameCacheSize:0 predrawingEnabled:YES];
|
||||
}
|
||||
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled
|
||||
{
|
||||
// Early return if no data supplied!
|
||||
const BOOL hasData = (data.length > 0);
|
||||
if (!hasData) {
|
||||
FLLog(FLLogLevelError, @"No animated GIF data supplied.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// Do one-time initializations of `readonly` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite` property overrides.
|
||||
|
||||
// Keep a strong reference to `data` and expose it read-only publicly.
|
||||
// However, we will use the `_imageSource` as handler to the image data throughout our life cycle.
|
||||
_data = data;
|
||||
_predrawingEnabled = isPredrawingEnabled;
|
||||
|
||||
// Initialize internal data structures
|
||||
_cachedFramesForIndexes = [[NSMutableDictionary alloc] init];
|
||||
_cachedFrameIndexes = [[NSMutableIndexSet alloc] init];
|
||||
_requestedFrameIndexes = [[NSMutableIndexSet alloc] init];
|
||||
|
||||
// Note: We could leverage `CGImageSourceCreateWithURL` too to add a second initializer `-initWithAnimatedGIFContentsOfURL:`.
|
||||
_imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data,
|
||||
(__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO});
|
||||
// Early return on failure!
|
||||
if (!_imageSource) {
|
||||
FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Early return if not GIF!
|
||||
const CFStringRef _Nullable imageSourceContainerType = CGImageSourceGetType(_imageSource);
|
||||
const BOOL isGIFData = imageSourceContainerType ? UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF) : NO;
|
||||
if (!isGIFData) {
|
||||
FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Get `LoopCount`
|
||||
// Note: 0 means repeating the animation indefinitely.
|
||||
// Image properties example:
|
||||
// {
|
||||
// FileSize = 314446;
|
||||
// "{GIF}" = {
|
||||
// HasGlobalColorMap = 1;
|
||||
// LoopCount = 0;
|
||||
// };
|
||||
// }
|
||||
NSDictionary *_Nullable const imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
|
||||
_loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
|
||||
|
||||
// Iterate through frame images
|
||||
const size_t imageCount = CGImageSourceGetCount(_imageSource);
|
||||
NSUInteger skippedFrameCount = 0;
|
||||
NSMutableDictionary *const delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
|
||||
for (size_t i = 0; i < imageCount; i++) {
|
||||
@autoreleasepool {
|
||||
const CGImageRef _Nullable frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
|
||||
if (frameImageRef) {
|
||||
UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef];
|
||||
// Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid).
|
||||
if (frameImage) {
|
||||
// Set poster image
|
||||
if (!self.posterImage) {
|
||||
_posterImage = frameImage;
|
||||
// Set its size to proxy our size.
|
||||
_size = _posterImage.size;
|
||||
// Remember index of poster image so we never purge it; also add it to the cache.
|
||||
_posterImageFrameIndex = i;
|
||||
[self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
|
||||
[self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
|
||||
}
|
||||
|
||||
// Get `DelayTime`
|
||||
// Note: It's not in (1/100) of a second like still falsely described in the documentation as per iOS 8 (rdar://19507384) but in seconds stored as `kCFNumberFloat32Type`.
|
||||
// Frame properties example:
|
||||
// {
|
||||
// ColorModel = RGB;
|
||||
// Depth = 8;
|
||||
// PixelHeight = 960;
|
||||
// PixelWidth = 640;
|
||||
// "{GIF}" = {
|
||||
// DelayTime = "0.4";
|
||||
// UnclampedDelayTime = "0.4";
|
||||
// };
|
||||
// }
|
||||
|
||||
NSDictionary *_Nullable const frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
|
||||
NSDictionary *_Nullable const framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
|
||||
|
||||
// Try to use the unclamped delay time; fall back to the normal delay time.
|
||||
NSNumber *_Nullable delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
|
||||
if (delayTime == nil) {
|
||||
delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
|
||||
}
|
||||
// If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value.
|
||||
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
|
||||
if (delayTime == nil) {
|
||||
if (i == 0) {
|
||||
FLLog(FLLogLevelInfo, @"Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties);
|
||||
delayTime = @(kDelayTimeIntervalDefault);
|
||||
} else {
|
||||
FLLog(FLLogLevelInfo, @"Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties);
|
||||
delayTime = delayTimesForIndexesMutable[@(i - 1)];
|
||||
}
|
||||
}
|
||||
// Support frame delays as low as `kFLAnimatedImageDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility.
|
||||
// To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO.
|
||||
if ([delayTime floatValue] < ((float)kFLAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) {
|
||||
FLLog(FLLogLevelInfo, @"Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kFLAnimatedImageDelayTimeIntervalMinimum);
|
||||
delayTime = @(kDelayTimeIntervalDefault);
|
||||
}
|
||||
delayTimesForIndexesMutable[@(i)] = delayTime;
|
||||
} else {
|
||||
skippedFrameCount++;
|
||||
FLLog(FLLogLevelInfo, @"Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef);
|
||||
}
|
||||
CFRelease(frameImageRef);
|
||||
} else {
|
||||
skippedFrameCount++;
|
||||
FLLog(FLLogLevelInfo, @"Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, self->_imageSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
_delayTimesForIndexes = [delayTimesForIndexesMutable copy];
|
||||
_frameCount = imageCount;
|
||||
|
||||
if (self.frameCount == 0) {
|
||||
FLLog(FLLogLevelInfo, @"Failed to create any valid frames for GIF with properties %@", imageProperties);
|
||||
return nil;
|
||||
} else if (self.frameCount == 1) {
|
||||
// Warn when we only have a single frame but return a valid GIF.
|
||||
FLLog(FLLogLevelInfo, @"Created valid GIF but with only a single frame. Image properties: %@", imageProperties);
|
||||
} else {
|
||||
// We have multiple frames, rock on!
|
||||
}
|
||||
|
||||
// If no value is provided, select a default based on the GIF.
|
||||
if (optimalFrameCacheSize == 0) {
|
||||
// Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size.
|
||||
// It's only dependent on the image size & number of frames and never changes.
|
||||
const CGFloat animatedImageDataSize = (CGFloat)CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * (CGFloat)(self.frameCount - skippedFrameCount) / (CGFloat)MEGABYTE;
|
||||
if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) {
|
||||
_frameCacheSizeOptimal = self.frameCount;
|
||||
} else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) {
|
||||
// This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames.
|
||||
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault;
|
||||
} else {
|
||||
// The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning.
|
||||
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory;
|
||||
}
|
||||
} else {
|
||||
// Use the provided value.
|
||||
_frameCacheSizeOptimal = optimalFrameCacheSize;
|
||||
}
|
||||
// In any case, cap the optimal cache size at the frame count.
|
||||
_frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount);
|
||||
|
||||
// Convenience/minor performance optimization; keep an index set handy with the full range to return in `-frameIndexesToCache`.
|
||||
_allFramesIndexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, self.frameCount)];
|
||||
|
||||
// See the property declarations for descriptions.
|
||||
_weakProxy = (id)[FLWeakProxy weakProxyForObject:self];
|
||||
|
||||
// Register this instance in the weak table for memory notifications. The NSHashTable will clean up after itself when we're gone.
|
||||
// Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
|
||||
@synchronized(allAnimatedImagesWeak) {
|
||||
[allAnimatedImagesWeak addObject:self];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
+ (instancetype)animatedImageWithGIFData:(NSData *)data
|
||||
{
|
||||
FLAnimatedImage *const animatedImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:data];
|
||||
return animatedImage;
|
||||
}
|
||||
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_weakProxy) {
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:_weakProxy];
|
||||
}
|
||||
|
||||
if (_imageSource) {
|
||||
CFRelease(_imageSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
// See header for more details.
|
||||
// Note: both consumer and producer are throttled: consumer by frame timings and producer by the available memory (max buffer window size).
|
||||
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index
|
||||
{
|
||||
// Early return if the requested index is beyond bounds.
|
||||
// Note: We're comparing an index with a count and need to bail on greater than or equal to.
|
||||
if (index >= self.frameCount) {
|
||||
FLLog(FLLogLevelWarn, @"Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Remember requested frame index, this influences what we should cache next.
|
||||
self.requestedFrameIndex = index;
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didRequestCachedFrame:)]) {
|
||||
[self.debug_delegate debug_animatedImage:self didRequestCachedFrame:index];
|
||||
}
|
||||
#endif
|
||||
|
||||
// Quick check to avoid doing any work if we already have all possible frames cached, a common case.
|
||||
if ([self.cachedFrameIndexes count] < self.frameCount) {
|
||||
// If we have frames that should be cached but aren't and aren't requested yet, request them.
|
||||
// Exclude existing cached frames, frames already requested, and specially cached poster image.
|
||||
NSMutableIndexSet *frameIndexesToAddToCacheMutable = [self frameIndexesToCache];
|
||||
[frameIndexesToAddToCacheMutable removeIndexes:self.cachedFrameIndexes];
|
||||
[frameIndexesToAddToCacheMutable removeIndexes:self.requestedFrameIndexes];
|
||||
[frameIndexesToAddToCacheMutable removeIndex:self.posterImageFrameIndex];
|
||||
NSIndexSet *frameIndexesToAddToCache = [frameIndexesToAddToCacheMutable copy];
|
||||
|
||||
// Asynchronously add frames to our cache.
|
||||
if ([frameIndexesToAddToCache count] > 0) {
|
||||
[self addFrameIndexesToCache:frameIndexesToAddToCache];
|
||||
}
|
||||
}
|
||||
|
||||
// Get the specified image.
|
||||
UIImage *const image = self.cachedFramesForIndexes[@(index)];
|
||||
|
||||
// Purge if needed based on the current playhead position.
|
||||
[self purgeFrameCacheIfNeeded];
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
// Only called once from `-imageLazilyCachedAtIndex` but factored into its own method for logical grouping.
|
||||
- (void)addFrameIndexesToCache:(NSIndexSet *)frameIndexesToAddToCache
|
||||
{
|
||||
// Order matters. First, iterate over the indexes starting from the requested frame index.
|
||||
// Then, if there are any indexes before the requested frame index, do those.
|
||||
const NSRange firstRange = NSMakeRange(self.requestedFrameIndex, self.frameCount - self.requestedFrameIndex);
|
||||
const NSRange secondRange = NSMakeRange(0, self.requestedFrameIndex);
|
||||
if (firstRange.length + secondRange.length != self.frameCount) {
|
||||
FLLog(FLLogLevelWarn, @"Two-part frame cache range doesn't equal full range.");
|
||||
}
|
||||
|
||||
// Add to the requested list before we actually kick them off, so they don't get into the queue twice.
|
||||
[self.requestedFrameIndexes addIndexes:frameIndexesToAddToCache];
|
||||
|
||||
// Lazily create dedicated isolation queue.
|
||||
if (!self.serialQueue) {
|
||||
_serialQueue = dispatch_queue_create("com.flipboard.framecachingqueue", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
|
||||
// Start streaming requested frames in the background into the cache.
|
||||
// Avoid capturing self in the block as there's no reason to keep doing work if the animated image went away.
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
// Produce and cache next needed frame.
|
||||
void (^frameRangeBlock)(NSRange, BOOL *) = ^(NSRange range, BOOL *stop) {
|
||||
// Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
|
||||
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
|
||||
#if defined(DEBUG) && DEBUG
|
||||
const CFTimeInterval predrawBeginTime = CACurrentMediaTime();
|
||||
#endif
|
||||
UIImage *const image = [weakSelf imageAtIndex:i];
|
||||
#if defined(DEBUG) && DEBUG
|
||||
const CFTimeInterval predrawDuration = CACurrentMediaTime() - predrawBeginTime;
|
||||
CFTimeInterval slowdownDuration = 0.0;
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImagePredrawingSlowdownFactor:)]) {
|
||||
CGFloat predrawingSlowdownFactor = [self.debug_delegate debug_animatedImagePredrawingSlowdownFactor:self];
|
||||
slowdownDuration = predrawDuration * predrawingSlowdownFactor - predrawDuration;
|
||||
[NSThread sleepForTimeInterval:slowdownDuration];
|
||||
}
|
||||
FLLog(FLLogLevelVerbose, @"Predrew frame %lu in %f ms for animated image: %@", (unsigned long)i, (predrawDuration + slowdownDuration) * 1000, self);
|
||||
#endif
|
||||
// The results get returned one by one as soon as they're ready (and not in batch).
|
||||
// The benefits of having the first frames as quick as possible outweigh building up a buffer to cope with potential hiccups when the CPU suddenly gets busy.
|
||||
if (image && weakSelf) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
weakSelf.cachedFramesForIndexes[@(i)] = image;
|
||||
[weakSelf.cachedFrameIndexes addIndex:i];
|
||||
[weakSelf.requestedFrameIndexes removeIndex:i];
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([weakSelf.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
|
||||
[weakSelf.debug_delegate debug_animatedImage:weakSelf didUpdateCachedFrames:weakSelf.cachedFrameIndexes];
|
||||
}
|
||||
#endif
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[frameIndexesToAddToCache enumerateRangesInRange:firstRange options:0 usingBlock:frameRangeBlock];
|
||||
[frameIndexesToAddToCache enumerateRangesInRange:secondRange options:0 usingBlock:frameRangeBlock];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+ (CGSize)sizeForImage:(id)image
|
||||
{
|
||||
CGSize imageSize = CGSizeZero;
|
||||
|
||||
// Early return for nil
|
||||
if (!image) {
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
if ([image isKindOfClass:[UIImage class]]) {
|
||||
UIImage *const uiImage = (UIImage *)image;
|
||||
imageSize = uiImage.size;
|
||||
} else if ([image isKindOfClass:[FLAnimatedImage class]]) {
|
||||
FLAnimatedImage *const animatedImage = (FLAnimatedImage *)image;
|
||||
imageSize = animatedImage.size;
|
||||
} else {
|
||||
// Bear trap to capture bad images; we have seen crashers cropping up on iOS 7.
|
||||
FLLog(FLLogLevelError, @"`image` isn't of expected types `UIImage` or `FLAnimatedImage`: %@", image);
|
||||
}
|
||||
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private Methods
|
||||
#pragma mark Frame Loading
|
||||
|
||||
- (UIImage *)imageAtIndex:(NSUInteger)index
|
||||
{
|
||||
// It's very important to use the cached `_imageSource` since the random access to a frame with `CGImageSourceCreateImageAtIndex` turns from an O(1) into an O(n) operation when re-initializing the image source every time.
|
||||
const CGImageRef _Nullable imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL);
|
||||
|
||||
// Early return for nil
|
||||
if (!imageRef) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithCGImage:imageRef];
|
||||
CFRelease(imageRef);
|
||||
|
||||
// Loading in the image object is only half the work, the displaying image view would still have to synchronosly wait and decode the image, so we go ahead and do that here on the background thread.
|
||||
if (self.isPredrawingEnabled) {
|
||||
image = [[self class] predrawnImageFromImage:image];
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Frame Caching
|
||||
|
||||
- (NSMutableIndexSet *)frameIndexesToCache
|
||||
{
|
||||
NSMutableIndexSet *indexesToCache = nil;
|
||||
// Quick check to avoid building the index set if the number of frames to cache equals the total frame count.
|
||||
if (self.frameCacheSizeCurrent == self.frameCount) {
|
||||
indexesToCache = [self.allFramesIndexSet mutableCopy];
|
||||
} else {
|
||||
indexesToCache = [[NSMutableIndexSet alloc] init];
|
||||
|
||||
// Add indexes to the set in two separate blocks- the first starting from the requested frame index, up to the limit or the end.
|
||||
// The second, if needed, the remaining number of frames beginning at index zero.
|
||||
const NSUInteger firstLength = MIN(self.frameCacheSizeCurrent, self.frameCount - self.requestedFrameIndex);
|
||||
const NSRange firstRange = NSMakeRange(self.requestedFrameIndex, firstLength);
|
||||
[indexesToCache addIndexesInRange:firstRange];
|
||||
const NSUInteger secondLength = self.frameCacheSizeCurrent - firstLength;
|
||||
if (secondLength > 0) {
|
||||
NSRange secondRange = NSMakeRange(0, secondLength);
|
||||
[indexesToCache addIndexesInRange:secondRange];
|
||||
}
|
||||
// Double check our math, before we add the poster image index which may increase it by one.
|
||||
if ([indexesToCache count] != self.frameCacheSizeCurrent) {
|
||||
FLLog(FLLogLevelWarn, @"Number of frames to cache doesn't equal expected cache size.");
|
||||
}
|
||||
|
||||
[indexesToCache addIndex:self.posterImageFrameIndex];
|
||||
}
|
||||
|
||||
return indexesToCache;
|
||||
}
|
||||
|
||||
|
||||
- (void)purgeFrameCacheIfNeeded
|
||||
{
|
||||
// Purge frames that are currently cached but don't need to be.
|
||||
// But not if we're still under the number of frames to cache.
|
||||
// This way, if all frames are allowed to be cached (the common case), we can skip all the `NSIndexSet` math below.
|
||||
if ([self.cachedFrameIndexes count] > self.frameCacheSizeCurrent) {
|
||||
NSMutableIndexSet *indexesToPurge = [self.cachedFrameIndexes mutableCopy];
|
||||
[indexesToPurge removeIndexes:[self frameIndexesToCache]];
|
||||
[indexesToPurge enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) {
|
||||
// Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
|
||||
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
|
||||
[self.cachedFrameIndexes removeIndex:i];
|
||||
[self.cachedFramesForIndexes removeObjectForKey:@(i)];
|
||||
// Note: Don't `CGImageSourceRemoveCacheAtIndex` on the image source for frames that we don't want cached any longer to maintain O(1) time access.
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.debug_delegate debug_animatedImage:self didUpdateCachedFrames:self.cachedFrameIndexes];
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)growFrameCacheSizeAfterMemoryWarning:(NSNumber *)frameCacheSize
|
||||
{
|
||||
self.frameCacheSizeMaxInternal = [frameCacheSize unsignedIntegerValue];
|
||||
FLLog(FLLogLevelDebug, @"Grew frame cache size max to %lu after memory warning for animated image: %@", (unsigned long)self.frameCacheSizeMaxInternal, self);
|
||||
|
||||
// Schedule resetting the frame cache size max completely after a while.
|
||||
const NSTimeInterval kResetDelay = 3.0;
|
||||
[self.weakProxy performSelector:@selector(resetFrameCacheSizeMaxInternal) withObject:nil afterDelay:kResetDelay];
|
||||
}
|
||||
|
||||
|
||||
- (void)resetFrameCacheSizeMaxInternal
|
||||
{
|
||||
self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeNoLimit;
|
||||
FLLog(FLLogLevelDebug, @"Reset frame cache size max (current frame cache size: %lu) for animated image: %@", (unsigned long)self.frameCacheSizeCurrent, self);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark System Memory Warnings Notification Handler
|
||||
|
||||
- (void)didReceiveMemoryWarning:(NSNotification *)notification
|
||||
{
|
||||
self.memoryWarningCount++;
|
||||
|
||||
// If we were about to grow larger, but got rapped on our knuckles by the system again, cancel.
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(growFrameCacheSizeAfterMemoryWarning:) object:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning)];
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(resetFrameCacheSizeMaxInternal) object:nil];
|
||||
|
||||
// Go down to the minimum and by that implicitly immediately purge from the cache if needed to not get jettisoned by the system and start producing frames on-demand.
|
||||
FLLog(FLLogLevelDebug, @"Attempt setting frame cache size max to %lu (previous was %lu) after memory warning #%lu for animated image: %@", (unsigned long)FLAnimatedImageFrameCacheSizeLowMemory, (unsigned long)self.frameCacheSizeMaxInternal, (unsigned long)self.memoryWarningCount, self);
|
||||
self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeLowMemory;
|
||||
|
||||
// Schedule growing larger again after a while, but cap our attempts to prevent a periodic sawtooth wave (ramps upward and then sharply drops) of memory usage.
|
||||
//
|
||||
// [mem]^ (2) (5) (6) 1) Loading frames for the first time
|
||||
// (*)| , , , 2) Mem warning #1; purge cache
|
||||
// | /| (4)/| /| 3) Grow cache size a bit after a while, if no mem warning occurs
|
||||
// | / | _/ | _/ | 4) Try to grow cache size back to optimum after a while, if no mem warning occurs
|
||||
// |(1)/ |_/ |/ |__(7) 5) Mem warning #2; purge cache
|
||||
// |__/ (3) 6) After repetition of (3) and (4), mem warning #3; purge cache
|
||||
// +----------------------> 7) After 3 mem warnings, stay at minimum cache size
|
||||
// [t]
|
||||
// *) The mem high water mark before we get warned might change for every cycle.
|
||||
//
|
||||
const NSUInteger kGrowAttemptsMax = 2;
|
||||
const NSTimeInterval kGrowDelay = 2.0;
|
||||
if ((self.memoryWarningCount - 1) <= kGrowAttemptsMax) {
|
||||
[self.weakProxy performSelector:@selector(growFrameCacheSizeAfterMemoryWarning:) withObject:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning) afterDelay:kGrowDelay];
|
||||
}
|
||||
|
||||
// Note: It's not possible to get the level of a memory warning with a public API: http://stackoverflow.com/questions/2915247/iphone-os-memory-warnings-what-do-the-different-levels-mean/2915477#2915477
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Image Decoding
|
||||
|
||||
// Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread.
|
||||
// On success, the returned object is a new `UIImage` instance with the same content as the one passed in.
|
||||
// On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged.
|
||||
// First inspired by & good Karma to: https://gist.github.com/steipete/1144242
|
||||
+ (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw
|
||||
{
|
||||
// Always use a device RGB color space for simplicity and predictability what will be going on.
|
||||
const CGColorSpaceRef _Nullable colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB();
|
||||
// Early return on failure!
|
||||
if (!colorSpaceDeviceRGBRef) {
|
||||
FLLog(FLLogLevelError, @"Failed to `CGColorSpaceCreateDeviceRGB` for image %@", imageToPredraw);
|
||||
return imageToPredraw;
|
||||
}
|
||||
|
||||
// Even when the image doesn't have transparency, we have to add the extra channel because Quartz doesn't support other pixel formats than 32 bpp/8 bpc for RGB:
|
||||
// kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaPremultipliedLast
|
||||
// (source: docs "Quartz 2D Programming Guide > Graphics Contexts > Table 2-1 Pixel formats supported for bitmap graphics contexts")
|
||||
const size_t numberOfComponents = CGColorSpaceGetNumberOfComponents(colorSpaceDeviceRGBRef) + 1; // 4: RGB + A
|
||||
|
||||
// "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs)
|
||||
void *_Nullable data = NULL;
|
||||
const size_t width = imageToPredraw.size.width;
|
||||
const size_t height = imageToPredraw.size.height;
|
||||
const size_t bitsPerComponent = CHAR_BIT;
|
||||
|
||||
const size_t bitsPerPixel = (bitsPerComponent * numberOfComponents);
|
||||
const size_t bytesPerPixel = (bitsPerPixel / BYTE_SIZE);
|
||||
const size_t bytesPerRow = (bytesPerPixel * width);
|
||||
|
||||
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
|
||||
|
||||
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage);
|
||||
// If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.
|
||||
// "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs)
|
||||
if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) {
|
||||
alphaInfo = kCGImageAlphaNoneSkipFirst;
|
||||
} else if (alphaInfo == kCGImageAlphaFirst) {
|
||||
alphaInfo = kCGImageAlphaPremultipliedFirst;
|
||||
} else if (alphaInfo == kCGImageAlphaLast) {
|
||||
alphaInfo = kCGImageAlphaPremultipliedLast;
|
||||
}
|
||||
// "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs)
|
||||
bitmapInfo |= alphaInfo;
|
||||
|
||||
// Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time).
|
||||
// Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why.
|
||||
const CGContextRef _Nullable bitmapContextRef = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpaceDeviceRGBRef, bitmapInfo);
|
||||
CGColorSpaceRelease(colorSpaceDeviceRGBRef);
|
||||
// Early return on failure!
|
||||
if (!bitmapContextRef) {
|
||||
FLLog(FLLogLevelError, @"Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw);
|
||||
return imageToPredraw;
|
||||
}
|
||||
|
||||
// Draw image in bitmap context and create image by preserving receiver's properties.
|
||||
CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, imageToPredraw.size.width, imageToPredraw.size.height), imageToPredraw.CGImage);
|
||||
const CGImageRef _Nullable predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef);
|
||||
UIImage *_Nullable predrawnImage = predrawnImageRef ? [UIImage imageWithCGImage:predrawnImageRef scale:imageToPredraw.scale orientation:imageToPredraw.imageOrientation] : nil;
|
||||
CGImageRelease(predrawnImageRef);
|
||||
CGContextRelease(bitmapContextRef);
|
||||
|
||||
// Early return on failure!
|
||||
if (!predrawnImage) {
|
||||
FLLog(FLLogLevelError, @"Failed to `imageWithCGImage:scale:orientation:` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw);
|
||||
return imageToPredraw;
|
||||
}
|
||||
|
||||
return predrawnImage;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Description
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *description = [super description];
|
||||
|
||||
description = [description stringByAppendingFormat:@" size=%@", NSStringFromCGSize(self.size)];
|
||||
description = [description stringByAppendingFormat:@" frameCount=%lu", (unsigned long)self.frameCount];
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
@implementation FLAnimatedImage (Logging)
|
||||
|
||||
static void (^_logBlock)(NSString *logString, FLLogLevel logLevel) = nil;
|
||||
static FLLogLevel _logLevel;
|
||||
|
||||
+ (void)setLogBlock:(void (^_Nullable)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel
|
||||
{
|
||||
_logBlock = [logBlock copy];
|
||||
_logLevel = logLevel;
|
||||
}
|
||||
|
||||
+ (void)logStringFromBlock:(NSString *(^_Nullable)(void))stringBlock withLevel:(FLLogLevel)level
|
||||
{
|
||||
if (level <= _logLevel && _logBlock && stringBlock) {
|
||||
_logBlock(stringBlock(), level);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - FLWeakProxy
|
||||
|
||||
@interface FLWeakProxy ()
|
||||
|
||||
@property (nonatomic, weak) id target;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLWeakProxy
|
||||
|
||||
#pragma mark Life Cycle
|
||||
|
||||
// This is the designated creation method of an `FLWeakProxy` and
|
||||
// as a subclass of `NSProxy` it doesn't respond to or need `-init`.
|
||||
+ (instancetype)weakProxyForObject:(id)targetObject
|
||||
{
|
||||
FLWeakProxy *weakProxy = [FLWeakProxy alloc];
|
||||
weakProxy.target = targetObject;
|
||||
return weakProxy;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Forwarding Messages
|
||||
|
||||
- (id)forwardingTargetForSelector:(SEL)selector
|
||||
{
|
||||
// Keep it lightweight: access the ivar directly
|
||||
return _target;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - NSWeakProxy Method Overrides
|
||||
#pragma mark Handling Unimplemented Methods
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||
{
|
||||
// Fallback for when target is nil. Don't do anything, just return 0/NULL/nil.
|
||||
// The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing.
|
||||
// We can't really handle struct return types here because we don't know the length.
|
||||
void *_Nullable nullPointer = NULL;
|
||||
[invocation setReturnValue:&nullPointer];
|
||||
}
|
||||
|
||||
|
||||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
|
||||
{
|
||||
// We only get here if `forwardingTargetForSelector:` returns nil.
|
||||
// In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing.
|
||||
// We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return value is `sizeof(void *)`.
|
||||
// Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than necessary and has issues as well.
|
||||
// See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache.
|
||||
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
460
Pods/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.m
generated
Executable file
460
Pods/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.m
generated
Executable file
@@ -0,0 +1,460 @@
|
||||
//
|
||||
// FLAnimatedImageView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "FLAnimatedImageView.h"
|
||||
#import "FLAnimatedImage.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@protocol FLAnimatedImageViewDebugDelegate <NSObject>
|
||||
@optional
|
||||
- (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration;
|
||||
@end
|
||||
#endif
|
||||
|
||||
|
||||
@interface FLAnimatedImageView ()
|
||||
|
||||
// Override of public `readonly` properties as private `readwrite`
|
||||
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
|
||||
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
|
||||
|
||||
@property (nonatomic, assign) NSUInteger loopCountdown;
|
||||
@property (nonatomic, assign) NSTimeInterval accumulator;
|
||||
@property (nonatomic, strong) CADisplayLink *displayLink;
|
||||
|
||||
@property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image or visibility (window, superview, hidden, alpha) has changed.
|
||||
@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@property (nonatomic, weak) id<FLAnimatedImageViewDebugDelegate> debug_delegate;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLAnimatedImageView
|
||||
@synthesize runLoopMode = _runLoopMode;
|
||||
|
||||
#pragma mark - Initializers
|
||||
|
||||
// -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be.
|
||||
// Using -initWithImage: doesn't call any of the other designated initializers.
|
||||
- (instancetype)initWithImage:(UIImage *)image
|
||||
{
|
||||
self = [super initWithImage:image];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers.
|
||||
- (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage
|
||||
{
|
||||
self = [super initWithImage:image highlightedImage:highlightedImage];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
self.runLoopMode = [[self class] defaultRunLoopMode];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.accessibilityIgnoresInvertColors = YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
#pragma mark Public
|
||||
|
||||
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
|
||||
{
|
||||
if (![_animatedImage isEqual:animatedImage]) {
|
||||
if (animatedImage) {
|
||||
if (super.image) {
|
||||
// UIImageView's `setImage:` will internally call its layer's `setContentsTransform:` based on the `image.imageOrientation`.
|
||||
// The `contentsTransform` will affect layer rendering rotation because the CGImage's bitmap buffer does not actually take rotation.
|
||||
// However, when calling `setImage:nil`, this `contentsTransform` will not be reset to identity.
|
||||
// Further animation frame will be rendered as rotated. So we must set it to the poster image to clear the previous state.
|
||||
// See more here: https://github.com/Flipboard/FLAnimatedImage/issues/100
|
||||
super.image = animatedImage.posterImage;
|
||||
// Clear out the image.
|
||||
super.image = nil;
|
||||
}
|
||||
// Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
|
||||
super.highlighted = NO;
|
||||
// UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
|
||||
[self invalidateIntrinsicContentSize];
|
||||
} else {
|
||||
// Stop animating before the animated image gets cleared out.
|
||||
[self stopAnimating];
|
||||
}
|
||||
|
||||
_animatedImage = animatedImage;
|
||||
|
||||
self.currentFrame = animatedImage.posterImage;
|
||||
self.currentFrameIndex = 0;
|
||||
if (animatedImage.loopCount > 0) {
|
||||
self.loopCountdown = animatedImage.loopCount;
|
||||
} else {
|
||||
self.loopCountdown = NSUIntegerMax;
|
||||
}
|
||||
self.accumulator = 0.0;
|
||||
|
||||
// Start animating after the new animated image has been set.
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
}
|
||||
|
||||
[self.layer setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Life Cycle
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
// Removes the display link from all run loop modes.
|
||||
[_displayLink invalidate];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIView Method Overrides
|
||||
#pragma mark Observing View-Related Changes
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
[super didMoveToWindow];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAlpha:(CGFloat)alpha
|
||||
{
|
||||
[super setAlpha:alpha];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden
|
||||
{
|
||||
[super setHidden:hidden];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Auto Layout
|
||||
|
||||
- (CGSize)intrinsicContentSize
|
||||
{
|
||||
// Default to let UIImageView handle the sizing of its image, and anything else it might consider.
|
||||
CGSize intrinsicContentSize = [super intrinsicContentSize];
|
||||
|
||||
// If we have have an animated image, use its image size.
|
||||
// UIImageView's intrinsic content size seems to be the size of its image. The obvious approach, simply calling `-invalidateIntrinsicContentSize` when setting an animated image, results in UIImageView steadfastly returning `{UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric}` for its intrinsicContentSize.
|
||||
// (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.)
|
||||
if (self.animatedImage) {
|
||||
intrinsicContentSize = self.image.size;
|
||||
}
|
||||
|
||||
return intrinsicContentSize;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIImageView Method Overrides
|
||||
#pragma mark Image Data
|
||||
|
||||
- (UIImage *)image
|
||||
{
|
||||
UIImage *image = nil;
|
||||
if (self.animatedImage) {
|
||||
// Initially set to the poster image.
|
||||
image = self.currentFrame;
|
||||
} else {
|
||||
image = super.image;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
- (void)setImage:(UIImage *)image
|
||||
{
|
||||
if (image) {
|
||||
// Clear out the animated image and implicitly pause animation playback.
|
||||
self.animatedImage = nil;
|
||||
}
|
||||
|
||||
super.image = image;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Animating Images
|
||||
|
||||
- (NSTimeInterval)frameDelayGreatestCommonDivisor
|
||||
{
|
||||
// Presision is set to half of the `kFLAnimatedImageDelayTimeIntervalMinimum` in order to minimize frame dropping.
|
||||
const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kFLAnimatedImageDelayTimeIntervalMinimum;
|
||||
|
||||
NSArray *const delays = self.animatedImage.delayTimesForIndexes.allValues;
|
||||
|
||||
// Scales the frame delays by `kGreatestCommonDivisorPrecision`
|
||||
// then converts it to an UInteger for in order to calculate the GCD.
|
||||
NSUInteger scaledGCD = lrint([delays.firstObject floatValue] * kGreatestCommonDivisorPrecision);
|
||||
for (NSNumber *value in delays) {
|
||||
scaledGCD = gcd(lrint([value floatValue] * kGreatestCommonDivisorPrecision), scaledGCD);
|
||||
}
|
||||
|
||||
// Reverse to scale to get the value back into seconds.
|
||||
return (double)scaledGCD / kGreatestCommonDivisorPrecision;
|
||||
}
|
||||
|
||||
|
||||
static NSUInteger gcd(NSUInteger a, NSUInteger b)
|
||||
{
|
||||
// http://en.wikipedia.org/wiki/Greatest_common_divisor
|
||||
if (a < b) {
|
||||
return gcd(b, a);
|
||||
} else if (a == b) {
|
||||
return b;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const NSUInteger remainder = a % b;
|
||||
if (remainder == 0) {
|
||||
return b;
|
||||
}
|
||||
a = b;
|
||||
b = remainder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)startAnimating
|
||||
{
|
||||
if (self.animatedImage) {
|
||||
// Lazily create the display link.
|
||||
if (!self.displayLink) {
|
||||
// It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
|
||||
// will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
|
||||
// independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
|
||||
// link which will lead to the deallocation of both the display link and the weak proxy.
|
||||
FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
|
||||
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
|
||||
|
||||
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
|
||||
}
|
||||
|
||||
if (@available(iOS 10, *)) {
|
||||
// Adjusting preferredFramesPerSecond allows us to skip unnecessary calls to displayDidRefresh: when showing GIFs
|
||||
// that don't animate quickly. Use ceil to err on the side of too many FPS so we don't miss a frame transition moment.
|
||||
self.displayLink.preferredFramesPerSecond = ceil(1.0 / [self frameDelayGreatestCommonDivisor]);
|
||||
} else {
|
||||
const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
|
||||
self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
|
||||
}
|
||||
self.displayLink.paused = NO;
|
||||
} else {
|
||||
[super startAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setRunLoopMode:(NSRunLoopMode)runLoopMode
|
||||
{
|
||||
if (![@[NSDefaultRunLoopMode, NSRunLoopCommonModes] containsObject:runLoopMode]) {
|
||||
NSAssert(NO, @"Invalid run loop mode: %@", runLoopMode);
|
||||
_runLoopMode = [[self class] defaultRunLoopMode];
|
||||
} else {
|
||||
_runLoopMode = runLoopMode;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopAnimating
|
||||
{
|
||||
if (self.animatedImage) {
|
||||
self.displayLink.paused = YES;
|
||||
} else {
|
||||
[super stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)isAnimating
|
||||
{
|
||||
BOOL isAnimating = NO;
|
||||
if (self.animatedImage) {
|
||||
isAnimating = self.displayLink && !self.displayLink.isPaused;
|
||||
} else {
|
||||
isAnimating = [super isAnimating];
|
||||
}
|
||||
return isAnimating;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Highlighted Image Unsupport
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
// Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
|
||||
if (!self.animatedImage) {
|
||||
[super setHighlighted:highlighted];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private Methods
|
||||
#pragma mark Animation
|
||||
|
||||
// Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
|
||||
// Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed.
|
||||
- (void)updateShouldAnimate
|
||||
{
|
||||
const BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
|
||||
self.shouldAnimate = self.animatedImage && isVisible;
|
||||
}
|
||||
|
||||
|
||||
- (void)displayDidRefresh:(CADisplayLink *)displayLink
|
||||
{
|
||||
// If for some reason a wild call makes it through when we shouldn't be animating, bail.
|
||||
// Early return!
|
||||
if (!self.shouldAnimate) {
|
||||
FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self);
|
||||
return;
|
||||
}
|
||||
|
||||
NSNumber *_Nullable const delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
|
||||
// If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
|
||||
if (delayTimeNumber != nil) {
|
||||
const NSTimeInterval delayTime = [delayTimeNumber floatValue];
|
||||
// If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
|
||||
UIImage *_Nullable const image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
|
||||
if (image) {
|
||||
FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
|
||||
self.currentFrame = image;
|
||||
if (self.needsDisplayWhenImageBecomesAvailable) {
|
||||
[self.layer setNeedsDisplay];
|
||||
self.needsDisplayWhenImageBecomesAvailable = NO;
|
||||
}
|
||||
|
||||
if (@available(iOS 10, *)) {
|
||||
self.accumulator += displayLink.targetTimestamp - CACurrentMediaTime();
|
||||
} else {
|
||||
self.accumulator += displayLink.duration * (NSTimeInterval)displayLink.frameInterval;
|
||||
}
|
||||
|
||||
// While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
|
||||
while (self.accumulator >= delayTime) {
|
||||
self.accumulator -= delayTime;
|
||||
self.currentFrameIndex++;
|
||||
if (self.currentFrameIndex >= self.animatedImage.frameCount) {
|
||||
// If we've looped the number of times that this animated image describes, stop looping.
|
||||
self.loopCountdown--;
|
||||
if (self.loopCompletionBlock) {
|
||||
self.loopCompletionBlock(self.loopCountdown);
|
||||
}
|
||||
|
||||
if (self.loopCountdown == 0) {
|
||||
[self stopAnimating];
|
||||
return;
|
||||
}
|
||||
self.currentFrameIndex = 0;
|
||||
}
|
||||
// Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
|
||||
// Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
|
||||
self.needsDisplayWhenImageBecomesAvailable = YES;
|
||||
}
|
||||
} else {
|
||||
FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
|
||||
if (@available(iOS 10, *)) {
|
||||
[self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:displayLink.targetTimestamp - CACurrentMediaTime()];
|
||||
} else {
|
||||
[self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:displayLink.duration * (NSTimeInterval)displayLink.frameInterval];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
self.currentFrameIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSRunLoopMode)defaultRunLoopMode
|
||||
{
|
||||
// Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
|
||||
return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - CALayerDelegate (Informal)
|
||||
#pragma mark Providing the Layer's Content
|
||||
|
||||
- (void)displayLayer:(CALayer *)layer
|
||||
{
|
||||
layer.contents = (__bridge id)self.image.CGImage;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
83
Pods/FLAnimatedImage/FLAnimatedImage/include/FLAnimatedImage.h
generated
Normal file
83
Pods/FLAnimatedImage/FLAnimatedImage/include/FLAnimatedImage.h
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// FLAnimatedImage.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
// Allow user classes conveniently just importing one header.
|
||||
#import "FLAnimatedImageView.h"
|
||||
|
||||
#ifndef NS_DESIGNATED_INITIALIZER
|
||||
#if __has_attribute(objc_designated_initializer)
|
||||
#define NS_DESIGNATED_INITIALIZER __attribute((objc_designated_initializer))
|
||||
#else
|
||||
#define NS_DESIGNATED_INITIALIZER
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extern const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum;
|
||||
|
||||
//
|
||||
// An `FLAnimatedImage`'s job is to deliver frames in a highly performant way and works in conjunction with `FLAnimatedImageView`.
|
||||
// It subclasses `NSObject` and not `UIImage` because it's only an "image" in the sense that a sea lion is a lion.
|
||||
// It tries to intelligently choose the frame cache size depending on the image and memory situation with the goal to lower CPU usage for smaller ones, lower memory usage for larger ones and always deliver frames for high performant play-back.
|
||||
// Note: `posterImage`, `size`, `loopCount`, `delayTimes` and `frameCount` don't change after successful initialization.
|
||||
//
|
||||
@interface FLAnimatedImage : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) UIImage *posterImage; // Guaranteed to be loaded; usually equivalent to `-imageLazilyCachedAtIndex:0`
|
||||
@property (nonatomic, assign, readonly) CGSize size; // The `.posterImage`'s `.size`
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger loopCount; // "The number of times to repeat an animated sequence." according to ImageIO (note the slightly different definition to Netscape 2.0 Loop Extension); 0 means repeating the animation forever
|
||||
@property (nonatomic, strong, readonly) NSDictionary *delayTimesForIndexes; // Of type `NSTimeInterval` boxed in `NSNumber`s
|
||||
@property (nonatomic, assign, readonly) NSUInteger frameCount; // Number of valid frames; equal to `[.delayTimes count]`
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; // Current size of intelligently chosen buffer window; can range in the interval [1..frameCount]
|
||||
@property (nonatomic, assign) NSUInteger frameCacheSizeMax; // Allow to cap the cache size; 0 means no specific limit (default)
|
||||
|
||||
// Intended to be called from main thread synchronously; will return immediately.
|
||||
// If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling.
|
||||
// After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache.
|
||||
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index;
|
||||
|
||||
// Pass either a `UIImage` or an `FLAnimatedImage` and get back its size
|
||||
+ (CGSize)sizeForImage:(id)image;
|
||||
|
||||
// On success, the initializers return an `FLAnimatedImage` with all fields initialized, on failure they return `nil` and an error will be logged.
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data;
|
||||
// Pass 0 for optimalFrameCacheSize to get the default, predrawing is enabled by default.
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled NS_DESIGNATED_INITIALIZER;
|
||||
+ (instancetype)animatedImageWithGIFData:(NSData *)data;
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only
|
||||
|
||||
@end
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLLogLevel) {
|
||||
FLLogLevelNone = 0,
|
||||
FLLogLevelError,
|
||||
FLLogLevelWarn,
|
||||
FLLogLevelInfo,
|
||||
FLLogLevelDebug,
|
||||
FLLogLevelVerbose
|
||||
};
|
||||
|
||||
@interface FLAnimatedImage (Logging)
|
||||
|
||||
+ (void)setLogBlock:(void (^)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel;
|
||||
+ (void)logStringFromBlock:(NSString *(^)(void))stringBlock withLevel:(FLLogLevel)level;
|
||||
|
||||
@end
|
||||
|
||||
#define FLLog(logLevel, format, ...) [FLAnimatedImage logStringFromBlock:^NSString *{ return [NSString stringWithFormat:(format), ## __VA_ARGS__]; } withLevel:(logLevel)]
|
||||
|
||||
@interface FLWeakProxy : NSProxy
|
||||
|
||||
+ (instancetype)weakProxyForObject:(id)targetObject;
|
||||
|
||||
@end
|
||||
36
Pods/FLAnimatedImage/FLAnimatedImage/include/FLAnimatedImageView.h
generated
Normal file
36
Pods/FLAnimatedImage/FLAnimatedImage/include/FLAnimatedImageView.h
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// FLAnimatedImageView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLAnimatedImage;
|
||||
@protocol FLAnimatedImageViewDebugDelegate;
|
||||
|
||||
|
||||
//
|
||||
// An `FLAnimatedImageView` can take an `FLAnimatedImage` and plays it automatically when in view hierarchy and stops when removed.
|
||||
// The animation can also be controlled with the `UIImageView` methods `-start/stop/isAnimating`.
|
||||
// It is a fully compatible `UIImageView` subclass and can be used as a drop-in component to work with existing code paths expecting to display a `UIImage`.
|
||||
// Under the hood it uses a `CADisplayLink` for playback, which can be inspected with `currentFrame` & `currentFrameIndex`.
|
||||
//
|
||||
@interface FLAnimatedImageView : UIImageView
|
||||
|
||||
// Setting `[UIImageView.image]` to a non-`nil` value clears out existing `animatedImage`.
|
||||
// And vice versa, setting `animatedImage` will initially populate the `[UIImageView.image]` to its `posterImage` and then start animating and hold `currentFrame`.
|
||||
@property (nonatomic, strong) FLAnimatedImage *animatedImage;
|
||||
@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
|
||||
|
||||
@property (nonatomic, strong, readonly) UIImage *currentFrame;
|
||||
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
|
||||
|
||||
// The animation runloop mode. Enables playback during scrolling by allowing timer events (i.e. animation) with NSRunLoopCommonModes.
|
||||
// To keep scrolling smooth on single-core devices such as iPhone 3GS/4 and iPod Touch 4th gen, the default run loop mode is NSDefaultRunLoopMode. Otherwise, the default is NSDefaultRunLoopMode.
|
||||
@property (nonatomic, copy) NSRunLoopMode runLoopMode;
|
||||
|
||||
@end
|
||||
21
Pods/FLAnimatedImage/LICENSE
generated
Normal file
21
Pods/FLAnimatedImage/LICENSE
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2016 Flipboard
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
122
Pods/FLAnimatedImage/README.md
generated
Normal file
122
Pods/FLAnimatedImage/README.md
generated
Normal file
@@ -0,0 +1,122 @@
|
||||
# [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) · [](https://github.com/Flipboard/FLAnimatedImage/blob/master/LICENSE) [](https://github.com/Flipboard/FLAnimatedImage/pulls)
|
||||
|
||||
FLAnimatedImage is a performant animated GIF engine for iOS:
|
||||
|
||||
- Plays multiple GIFs simultaneously with a playback speed comparable to desktop browsers
|
||||
- Honors variable frame delays
|
||||
- Behaves gracefully under memory pressure
|
||||
- Eliminates delays or blocking during the first playback loop
|
||||
- Interprets the frame delays of fast GIFs the same way modern browsers do
|
||||
|
||||
It's a well-tested [component that powers all GIFs in Flipboard](http://engineering.flipboard.com/2014/05/animated-gif). To understand its behavior it comes with an interactive demo:
|
||||
|
||||

|
||||
|
||||
## Who is this for?
|
||||
|
||||
- Apps that don't support animated GIFs yet
|
||||
- Apps that already support animated GIFs but want a higher performance solution
|
||||
- People who want to tinker with the code ([the corresponding blog post](http://engineering.flipboard.com/2014/05/animated-gif/) is a great place to start; also see the *To Do* section below)
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
FLAnimatedImage is a well-encapsulated drop-in component. Simply replace your `UIImageView` instances with instances of `FLAnimatedImageView` to get animated GIF support. There is no central cache or state to manage.
|
||||
|
||||
If using CocoaPods, the quickest way to try it out is to type this on the command line:
|
||||
|
||||
```shell
|
||||
$ pod try FLAnimatedImage
|
||||
```
|
||||
|
||||
To add it to your app, copy the two classes `FLAnimatedImage.h/.m` and `FLAnimatedImageView.h/.m` into your Xcode project or add via [CocoaPods](http://cocoapods.org) by adding this to your Podfile:
|
||||
|
||||
```ruby
|
||||
pod 'FLAnimatedImage', '~> 1.0'
|
||||
```
|
||||
|
||||
If using [Carthage](https://github.com/Carthage/Carthage), add the following line into your `Cartfile`
|
||||
|
||||
```
|
||||
github "Flipboard/FLAnimatedImage"
|
||||
```
|
||||
|
||||
If using [Swift Package Manager](https://github.com/apple/swift-package-manager), add the following to your `Package.swift` or add via XCode:
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Flipboard/FLAnimatedImage.git", .upToNextMajor(from: "1.0.16"))
|
||||
],
|
||||
targets: [
|
||||
.target(name: "TestProject", dependencies: ["FLAnimatedImage""])
|
||||
]
|
||||
```
|
||||
|
||||
In your code, `#import "FLAnimatedImage.h"`, create an image from an animated GIF, and setup the image view to display it:
|
||||
|
||||
```objective-c
|
||||
FLAnimatedImage *image = [FLAnimatedImage animatedImageWithGIFData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif"]]];
|
||||
FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init];
|
||||
imageView.animatedImage = image;
|
||||
imageView.frame = CGRectMake(0.0, 0.0, 100.0, 100.0);
|
||||
[self.view addSubview:imageView];
|
||||
```
|
||||
|
||||
It's flexible to integrate in your custom image loading stack and backwards compatible to iOS 9.
|
||||
|
||||
It uses ARC and the Apple frameworks `QuartzCore`, `ImageIO`, `MobileCoreServices`, and `CoreGraphics`.
|
||||
|
||||
It is capable of fine-grained logging. A block can be set on `FLAnimatedImage` that's invoked when logging occurs with various log levels via the `+setLogBlock:logLevel:` method. For example:
|
||||
|
||||
```objective-c
|
||||
// Set up FLAnimatedImage logging.
|
||||
[FLAnimatedImage setLogBlock:^(NSString *logString, FLLogLevel logLevel) {
|
||||
// Using NSLog
|
||||
NSLog(@"%@", logString);
|
||||
|
||||
// ...or CocoaLumberjackLogger only logging warnings and errors
|
||||
if (logLevel == FLLogLevelError) {
|
||||
DDLogError(@"%@", logString);
|
||||
} else if (logLevel == FLLogLevelWarn) {
|
||||
DDLogWarn(@"%@", logString);
|
||||
}
|
||||
} logLevel:FLLogLevelWarn];
|
||||
```
|
||||
|
||||
Since FLAnimatedImage is licensed under MIT, it's compatible with the terms of using it for any app on the App Store.
|
||||
|
||||
## Release process
|
||||
1. Bump version in `FLAnimatedImage.podspec`, update CHANGES, and commit.
|
||||
2. Tag commit with `> git tag -a <VERSION> -m "<VERSION>"` and `> git push --tags`.
|
||||
3. [Submit Podspec to Trunk with](https://guides.cocoapods.org/making/specs-and-specs-repo.html#how-do-i-update-an-existing-pod) `> pod trunk push FLAnimatedImage.podspec` ([ensure you're auth'ed](https://guides.cocoapods.org/making/getting-setup-with-trunk.html#getting-started)).
|
||||
## To Do
|
||||
- Support other animated image formats such as APNG or WebP (WebP support implemented [here](https://github.com/Flipboard/FLAnimatedImage/pull/86))
|
||||
- Integration into network libraries and image caches
|
||||
- Investigate whether `FLAnimatedImage` should become a `UIImage` subclass
|
||||
- Smarter buffering
|
||||
- Bring demo app to iPhone
|
||||
|
||||
This code has successfully shipped to many people as is, but please do come with your questions, issues and pull requests!
|
||||
|
||||
## Select apps using FLAnimatedImage
|
||||
(alphabetically)
|
||||
|
||||
- [Close-up](http://closeu.pe)
|
||||
- [Design Shots](https://itunes.apple.com/app/id792517951)
|
||||
- [Dropbox](https://www.dropbox.com)
|
||||
- [Dumpert](http://dumpert.nl)
|
||||
- [Ello](https://ello.co/)
|
||||
- [Facebook](https://facebook.com)
|
||||
- [Flipboard](https://flipboard.com)
|
||||
- [getGIF](https://itunes.apple.com/app/id964784701)
|
||||
- [Gifalicious](https://itunes.apple.com/us/app/gifalicious-see-your-gifs/id965346708?mt=8)
|
||||
- [HashPhotos](https://itunes.apple.com/app/id685784609)
|
||||
- [Instagram](https://www.instagram.com/)
|
||||
- [LiveBooth](http://www.liveboothapp.com)
|
||||
- [lWlVl Festival](http://lwlvl.com)
|
||||
- [Medium](https://medium.com)
|
||||
- [Pinterest](https://pinterest.com)
|
||||
- [Slack](https://slack.com/)
|
||||
- [Telegram](https://telegram.org/)
|
||||
- [Zip Code Finder](https://itunes.apple.com/app/id893031254)
|
||||
|
||||
If you're using FLAnimatedImage in your app, please open a PR to add it to this list!
|
||||
21
Pods/HWPanModal/LICENSE
generated
Normal file
21
Pods/HWPanModal/LICENSE
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Heath Wang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
330
Pods/HWPanModal/README-CN.md
generated
Normal file
330
Pods/HWPanModal/README-CN.md
generated
Normal file
@@ -0,0 +1,330 @@
|
||||
|
||||
# HWPanModal 👍
|
||||
<p style="align: left">
|
||||
<a href="https://cocoapods.org/pods/HWPanModal">
|
||||
<img src="https://img.shields.io/cocoapods/v/HWPanModal.svg?style=flat">
|
||||
</a>
|
||||
<a href="https://cocoapods.org/pods/HWPanModal">
|
||||
<img src="https://img.shields.io/cocoapods/p/HWPanModal.svg?style=flat">
|
||||
</a>
|
||||
<a href="https://cocoapods.org/pods/HWPanModal">
|
||||
<img src="https://img.shields.io/badge/support-ios%208%2B-orange.svg">
|
||||
</a>
|
||||
<a href="https://cocoapods.org/pods/HWPanModal">
|
||||
<img src="https://img.shields.io/badge/language-objective--c-blue.svg">
|
||||
</a>
|
||||
<a href="https://travis-ci.org/HeathWang/HWPanModal">
|
||||
<img src="https://travis-ci.org/HeathWang/HWPanModal.svg?branch=master">
|
||||
</a>
|
||||
<a href="https://codebeat.co/projects/github-com-heathwang-hwpanmodal-master">
|
||||
<img alt="codebeat badge" src="https://codebeat.co/badges/fb96e7ea-2320-4219-8f19-777674a97d0e" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
HWPanModal 用于从底部弹出控制器(UIViewController),并用拖拽手势来关闭控制器。提供了自定义视图大小和位置,高度自定义弹出视图的各个属性。
|
||||
|
||||
APP中常见的从底部弹出视图,可以通过该框架快速实现,只需专注于相应的视图编写。常规热门app的UI示例:
|
||||
1. 知乎APP的查看评论
|
||||
2. 抖音的评论查看
|
||||
3. 微信,网易音乐等app弹出分享
|
||||
4. 嘀嗒出行行程进行页(地图上的浮层view效果)
|
||||
5. iOS13 默认模态(present)效果
|
||||
6. And more...
|
||||
|
||||
## 特别感谢
|
||||
|
||||
Special Thanks to JetBrains! I use AppCode IDE to develop my open source project.
|
||||
<p style="align: left">
|
||||
<a href="https://www.jetbrains.com/?from=HWPanModal">
|
||||
<img src="images/icon_AppCode.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 截图
|
||||
|
||||
<div style="text-align: center">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Basic</th>
|
||||
<th>Blur background</th>
|
||||
<th>Keyboard handle</th>
|
||||
<th>App demo</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: center">
|
||||
<img src="images/HWPanModal_example.gif" width="180" />
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<img src="images/HWPanModal_example_3.gif" width="180"/>
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<img src="images/HWPanModal_example_4.gif" width="180"/>
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<img src="images/HWPanModal_example_2.gif" width="180"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
## 功能
|
||||
1. 支持任意类型的 `UIViewController`
|
||||
2. 支持继承自 `HWPanModalContentView` 的view
|
||||
3. 平滑的转场动画
|
||||
4. 支持2种类型的手势dismiss视图
|
||||
1. 上下方向拖动关闭视图。
|
||||
2. 侧滑关闭视图,支持全屏侧滑。
|
||||
5. 支持为presenting VC编写自定义动画。
|
||||
6. 支持配置动画时间,动画options,弹性spring值
|
||||
7. 支持配置背景alpha,或者高斯模糊背景。注意:动态调整模糊效果仅工作于iOS9.0+。
|
||||
8. 支持显示隐藏指示器,修改圆角
|
||||
9. 自动处理键盘弹出消失事件。
|
||||
10. 自定义指示器indicator view。
|
||||
11. 事件可以穿透到下层presenting VC。
|
||||
12. 可配置presented content 阴影。
|
||||
|
||||
更多配置信息请参阅 [_HWPanModalPresentable.h_](https://github.com/HeathWang/HWPanModal/blob/master/Sources/Presentable/HWPanModalPresentable.h) 声明。
|
||||
|
||||
## 特别注意
|
||||
|
||||
1. 任何情况下,内部嵌套scrollable(UIScrollView,UITableView,UIWebView,UICollectionView),如果scrollable的contentSize变化了,务必调用`- (void)hw_panModalSetNeedsLayoutUpdate`刷新UI!!!
|
||||
2. 如果需要弹出浮层后push到下一层,使用`HWPanModalContentView`或者present vc用navigation 包一层。
|
||||
3. 请仔细阅读md,编译run示例代码,95%的功能在示例中都有展示,不要什么都不看就来问问题!!!
|
||||
|
||||
|
||||
### 支持UIViewController和继承自HWPanModalContentView弹出视图
|
||||
|
||||
从0.6.0版本后, 该框架支持使用 `HWPanModalContentView` 从底部弹出视图, 即实现了present ViewController同样的交互和动画。
|
||||
|
||||
不同点是 `HWPanModalContentView` 只是一个view视图, 通过添加一些动画实现了原本的功能。不像present ViewController的模式,你可以获得controller的整个生命周期,并且可以使用navigation栈来push VC。
|
||||
|
||||
`HWPanModalContentView` 目前的限制:
|
||||
* 不支持转屏。
|
||||
* 不支持屏幕边缘横向拖拽来dismiss。
|
||||
* 不支持自定义presenting VC动画。(因为是view,没有presenting VC)
|
||||
|
||||
|
||||
## 适配
|
||||
**iOS 8.0+**, support Objective-C & Swift.
|
||||
|
||||
## 安装
|
||||
|
||||
### [CocoaPods](https://guides.cocoapods.org/using/using-cocoapods.html)
|
||||
|
||||
```ruby
|
||||
pod 'HWPanModal', '~> 0.9.4'
|
||||
```
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 如何从底部弹出控制器
|
||||
只需要视图控制器适配 `HWPanModalPresentable` 协议即可. 默认情况下,不用重写适配的各个方法,如果需要自定义,请实现协议方法。
|
||||
|
||||
更多的自定义UI配置,请参见`HWPanModalPresentable`协议中每个方法的说明。
|
||||
|
||||
```Objective-C
|
||||
#import <HWPanModal/HWPanModal.h>
|
||||
@interface HWBaseViewController () <HWPanModalPresentable>
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWBaseViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentable
|
||||
- (PanModalHeight)longFormHeight {
|
||||
return PanModalHeightMake(PanModalHeightTypeMaxTopInset, 44);
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
弹出控制器:
|
||||
|
||||
```Objective-C
|
||||
#import <HWPanModal/HWPanModal.h>
|
||||
[self presentPanModal:[HWBaseViewController new]];
|
||||
```
|
||||
|
||||
就是这么简单。
|
||||
|
||||
### 如何主动更新控制器UI
|
||||
请查阅 `UIViewController+Presentation.h`,里面有详细说明。
|
||||
* Change the state between short and long form. call `- (void)hw_panModalTransitionTo:(PresentationState)state;`
|
||||
* Change ScrollView ContentOffset. call `- (void)hw_panModalSetContentOffset:(CGPoint)offset;`
|
||||
* Reload layout. call `- (void)hw_panModalSetNeedsLayoutUpdate;`
|
||||
* 注意:如果scrollable view的contentSize改变了,你必须调用改reload方法来更新UI。
|
||||
|
||||
### 自定义presenting VC动画编写
|
||||
|
||||
1. Create object conforms `HWPresentingViewControllerAnimatedTransitioning` .
|
||||
|
||||
```Objective-C
|
||||
|
||||
@interface HWMyCustomAnimation : NSObject <HWPresentingViewControllerAnimatedTransitioning>
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWMyCustomAnimation
|
||||
|
||||
|
||||
- (void)presentAnimateTransition:(id<HWPresentingViewControllerContextTransitioning>)transitionContext {
|
||||
NSTimeInterval duration = [transitionContext transitionDuration];
|
||||
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
// replace it.
|
||||
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
||||
fromVC.view.transform = CGAffineTransformMakeScale(0.95, 0.95);
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAnimateTransition:(id<HWPresentingViewControllerContextTransitioning>)transitionContext {
|
||||
NSTimeInterval duration = [transitionContext transitionDuration];
|
||||
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
// replace it.
|
||||
[UIView animateWithDuration:duration animations:^{
|
||||
toVC.view.transform = CGAffineTransformIdentity;
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
1. Overwrite below two method.
|
||||
|
||||
```Objective-C
|
||||
- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle {
|
||||
return PresentingViewControllerAnimationStyleCustom;
|
||||
}
|
||||
|
||||
- (id<HWPresentingViewControllerAnimatedTransitioning>)customPresentingVCAnimation {
|
||||
return self.customAnimation;
|
||||
}
|
||||
|
||||
- (HWMyCustomAnimation *)customAnimation {
|
||||
if (!_customAnimation) {
|
||||
_customAnimation = [HWMyCustomAnimation new];
|
||||
}
|
||||
return _customAnimation;
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义指示器indicator view
|
||||
|
||||
You just need to create your own UIView, then adopt `HWPanModalIndicatorProtocol`.
|
||||
|
||||
In your presented controller, return it:
|
||||
|
||||
```Objective-C
|
||||
- (nullable UIView <HWPanModalIndicatorProtocol> *)customIndicatorView {
|
||||
HWTextIndicatorView *textIndicatorView = [HWTextIndicatorView new];
|
||||
return textIndicatorView;
|
||||
}
|
||||
```
|
||||
|
||||
Here is `HWTextIndicatorView` code:
|
||||
|
||||
```Objective-C
|
||||
@interface HWTextIndicatorView : UIView <HWPanModalIndicatorProtocol>
|
||||
|
||||
@end
|
||||
|
||||
@interface HWTextIndicatorView ()
|
||||
@property (nonatomic, strong) UILabel *stateLabel;
|
||||
@end
|
||||
|
||||
@implementation HWTextIndicatorView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
// init the _stateLabel
|
||||
[self addSubview:_stateLabel];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (void)didChangeToState:(HWIndicatorState)state {
|
||||
switch (state) {
|
||||
case HWIndicatorStateNormal: {
|
||||
self.stateLabel.text = @"Please pull down to dismiss";
|
||||
self.stateLabel.textColor = [UIColor whiteColor];
|
||||
}
|
||||
break;
|
||||
case HWIndicatorStatePullDown: {
|
||||
self.stateLabel.text = @"Keep pull down to dismiss";
|
||||
self.stateLabel.textColor = [UIColor colorWithRed:1.000 green:0.200 blue:0.000 alpha:1.00];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)indicatorSize {
|
||||
return CGSizeMake(200, 18);
|
||||
}
|
||||
|
||||
- (void)setupSubviews {
|
||||
self.stateLabel.frame = self.bounds;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
### 如何使用HWPanModalContentView
|
||||
|
||||
你必须继承自 `HWPanModalContentView`. `HWPanModalContentView` 适配 `HWPanModalPresentable` 协议,就像你可用该协议来present一样。
|
||||
|
||||
```Objective-C
|
||||
@interface HWSimplePanModalView : HWPanModalContentView
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWSimplePanModalView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
// add view and layout.
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// present it.
|
||||
HWSimplePanModalView *simplePanModalView = [HWSimplePanModalView new];
|
||||
[simplePanModalView presentInView:nil];
|
||||
```
|
||||
|
||||
|
||||
## 例子
|
||||
|
||||
1. 克隆项目
|
||||
2. 然后执行 `pod install`
|
||||
3. 打开 HWPanModal.xcworkspace, 选择OC或者Swift项目运行
|
||||
|
||||
###### 我分别编写了纯`Objective-C` & `Swift`例子,基本涵盖了该framework的所有API使用。
|
||||
|
||||
## 联系我
|
||||
|
||||
Heath Wang
|
||||
yishu.jay@gmail.com
|
||||
|
||||
## WX
|
||||
|
||||
<p style="align: left">
|
||||
<a>
|
||||
<img src="images/groupChat.jpg" width="277">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## License
|
||||
|
||||
<b>HWPanModal</b> is released under a MIT License. See LICENSE file for details.
|
||||
|
||||
|
||||
27
Pods/HWPanModal/Sources/Animator/HWPanModalAnimator.h
generated
Normal file
27
Pods/HWPanModal/Sources/Animator/HWPanModalAnimator.h
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// HWPanModalAnimator.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void(^AnimationBlockType)(void);
|
||||
typedef void(^AnimationCompletionType)(BOOL completion);
|
||||
|
||||
static NSTimeInterval kTransitionDuration = 0.5;
|
||||
|
||||
@interface HWPanModalAnimator : NSObject
|
||||
|
||||
+ (void)animate:(AnimationBlockType)animations config:(nullable id <HWPanModalPresentable>)config completion:(nullable AnimationCompletionType)completion;
|
||||
|
||||
+ (void)dismissAnimate:(AnimationBlockType)animations config:(nullable id <HWPanModalPresentable>)config completion:(AnimationCompletionType)completion;
|
||||
|
||||
+ (void)smoothAnimate:(AnimationBlockType)animations duration:(NSTimeInterval)duration completion:(nullable AnimationCompletionType)completion;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
40
Pods/HWPanModal/Sources/Animator/HWPanModalAnimator.m
generated
Normal file
40
Pods/HWPanModal/Sources/Animator/HWPanModalAnimator.m
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// HWPanModalAnimator.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import "HWPanModalAnimator.h"
|
||||
|
||||
@implementation HWPanModalAnimator
|
||||
|
||||
+ (void)animate:(AnimationBlockType)animations config:(nullable id<HWPanModalPresentable>)config completion:(AnimationCompletionType)completion {
|
||||
[HWPanModalAnimator animate:animations config:config startingFromPercent:1 isPresentation:YES completion:completion];
|
||||
}
|
||||
|
||||
+ (void)dismissAnimate:(AnimationBlockType)animations config:(nullable id<HWPanModalPresentable>)config completion:(AnimationCompletionType)completion {
|
||||
[HWPanModalAnimator animate:animations config:config startingFromPercent:1 isPresentation:NO completion:completion];
|
||||
}
|
||||
|
||||
+ (void)animate:(AnimationBlockType)animations config:(nullable id <HWPanModalPresentable>)config startingFromPercent:(CGFloat)animationPercent isPresentation:(BOOL)flag completion:(AnimationCompletionType)completion {
|
||||
|
||||
NSTimeInterval duration;
|
||||
if (flag) {
|
||||
duration = config ? [config transitionDuration] : kTransitionDuration;
|
||||
} else {
|
||||
duration = config ? [config dismissalDuration] : kTransitionDuration;
|
||||
}
|
||||
|
||||
duration = duration * MAX(animationPercent, 0);
|
||||
CGFloat springDamping = config ? [config springDamping] : 1.0;
|
||||
UIViewAnimationOptions options = config ? [config transitionAnimationOptions] : UIViewAnimationOptionPreferredFramesPerSecondDefault;
|
||||
|
||||
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:springDamping initialSpringVelocity:0 options:options animations:animations completion:completion];
|
||||
}
|
||||
|
||||
+ (void)smoothAnimate:(AnimationBlockType)animations duration:(NSTimeInterval)duration completion:(nullable AnimationCompletionType)completion {
|
||||
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:animations completion:completion];
|
||||
}
|
||||
|
||||
@end
|
||||
17
Pods/HWPanModal/Sources/Animator/HWPanModalInteractiveAnimator.h
generated
Normal file
17
Pods/HWPanModal/Sources/Animator/HWPanModalInteractiveAnimator.h
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// HWPanModalInteractiveAnimator.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/14.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanModalInteractiveAnimator : UIPercentDrivenInteractiveTransition
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
16
Pods/HWPanModal/Sources/Animator/HWPanModalInteractiveAnimator.m
generated
Normal file
16
Pods/HWPanModal/Sources/Animator/HWPanModalInteractiveAnimator.m
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// HWPanModalInteractiveAnimator.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/14.
|
||||
//
|
||||
|
||||
#import "HWPanModalInteractiveAnimator.h"
|
||||
|
||||
@implementation HWPanModalInteractiveAnimator
|
||||
|
||||
- (CGFloat)completionSpeed {
|
||||
return 0.618;
|
||||
}
|
||||
|
||||
@end
|
||||
28
Pods/HWPanModal/Sources/Animator/HWPanModalPresentationAnimator.h
generated
Normal file
28
Pods/HWPanModal/Sources/Animator/HWPanModalPresentationAnimator.h
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// HWPanModalPresentationAnimator.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HWPanModalPresentationDelegate.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, TransitionStyle) {
|
||||
TransitionStylePresentation,
|
||||
TransitionStyleDismissal,
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanModalPresentationAnimator : NSObject <UIViewControllerAnimatedTransitioning>
|
||||
|
||||
- (instancetype)initWithTransitionStyle:(TransitionStyle)transitionStyle interactiveMode:(PanModalInteractiveMode)mode;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
314
Pods/HWPanModal/Sources/Animator/HWPanModalPresentationAnimator.m
generated
Normal file
314
Pods/HWPanModal/Sources/Animator/HWPanModalPresentationAnimator.m
generated
Normal file
@@ -0,0 +1,314 @@
|
||||
//
|
||||
// HWPanModalPresentationAnimator.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import "HWPanModalPresentationAnimator.h"
|
||||
#import "HWPanModalAnimator.h"
|
||||
#import "UIViewController+LayoutHelper.h"
|
||||
#import "HWPanContainerView.h"
|
||||
#import "UIView+HW_Frame.h"
|
||||
#import "HWPageSheetPresentingAnimation.h"
|
||||
#import "HWShoppingCartPresentingAnimation.h"
|
||||
|
||||
@interface HWPresentingVCTransitionContext : NSObject <HWPresentingViewControllerContextTransitioning>
|
||||
|
||||
@property (nonatomic, weak) UIViewController *fromVC;
|
||||
@property (nonatomic, weak) UIViewController *toVC;
|
||||
@property (nonatomic, assign) NSTimeInterval duration;
|
||||
@property (nonatomic, strong) UIView *containerView;
|
||||
|
||||
- (instancetype)initWithFromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC duration:(NSTimeInterval)duration containerView:(UIView *)containerView;
|
||||
|
||||
@end
|
||||
|
||||
@interface HWPanModalPresentationAnimator ()
|
||||
|
||||
@property (nonatomic, assign) TransitionStyle transitionStyle;
|
||||
|
||||
@property (nullable, nonatomic, strong) UISelectionFeedbackGenerator *feedbackGenerator API_AVAILABLE(ios(10.0));
|
||||
@property (nonatomic, strong) HWPresentingVCTransitionContext *presentingVCTransitionContext;
|
||||
@property (nonatomic, assign) PanModalInteractiveMode interactiveMode;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalPresentationAnimator
|
||||
|
||||
- (instancetype)initWithTransitionStyle:(TransitionStyle)transitionStyle interactiveMode:(PanModalInteractiveMode)mode {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_transitionStyle = transitionStyle;
|
||||
_interactiveMode = mode;
|
||||
if (transitionStyle == TransitionStylePresentation) {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
_feedbackGenerator = [UISelectionFeedbackGenerator new];
|
||||
[_feedbackGenerator prepare];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出controller动画
|
||||
*/
|
||||
- (void)animatePresentation:(id<UIViewControllerContextTransitioning>)context {
|
||||
|
||||
UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
if (!toVC && !fromVC)
|
||||
return;
|
||||
|
||||
UIViewController<HWPanModalPresentable> *presentable = [self panModalViewController:context];
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
// If you are implementing a custom container controller, use this method to tell the child that its views are about to appear or disappear.
|
||||
[fromVC beginAppearanceTransition:NO animated:YES];
|
||||
[self beginAppearanceTransitionForController:toVC isAppearing:YES animated:YES];
|
||||
}
|
||||
|
||||
|
||||
CGFloat yPos = presentable.shortFormYPos;
|
||||
if ([presentable originPresentationState] == PresentationStateLong) {
|
||||
yPos = presentable.longFormYPos;
|
||||
} else if ([presentable originPresentationState] == PresentationStateMedium) {
|
||||
yPos = presentable.mediumFormYPos;
|
||||
}
|
||||
|
||||
UIView *panView = context.containerView.panContainerView ?: toVC.view;
|
||||
panView.frame = [context finalFrameForViewController:toVC];
|
||||
panView.hw_top = context.containerView.frame.size.height;
|
||||
|
||||
if ([presentable isHapticFeedbackEnabled]) {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
[self.feedbackGenerator selectionChanged];
|
||||
}
|
||||
}
|
||||
|
||||
[HWPanModalAnimator animate:^{
|
||||
panView.hw_top = yPos;
|
||||
} config:presentable completion:^(BOOL completion) {
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
[fromVC endAppearanceTransition];
|
||||
[self endAppearanceTransitionForController:toVC];
|
||||
}
|
||||
|
||||
if (@available(iOS 10.0, *)) {
|
||||
self.feedbackGenerator = nil;
|
||||
}
|
||||
|
||||
[context completeTransition:completion];
|
||||
}];
|
||||
|
||||
self.presentingVCTransitionContext = [[HWPresentingVCTransitionContext alloc] initWithFromVC:fromVC toVC:toVC duration:[presentable transitionDuration] containerView:context.containerView];
|
||||
[self presentAnimationForPresentingVC:presentable];
|
||||
}
|
||||
|
||||
/**
|
||||
* 使弹出controller消失动画
|
||||
*/
|
||||
- (void)animateDismissal:(id<UIViewControllerContextTransitioning>)context {
|
||||
|
||||
UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
if (!fromVC && !toVC)
|
||||
return;
|
||||
|
||||
UIViewController<HWPanModalPresentable> *presentable = [self panModalViewController:context];
|
||||
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
[self beginAppearanceTransitionForController:fromVC isAppearing:NO animated:YES];
|
||||
[toVC beginAppearanceTransition:YES animated:YES];
|
||||
}
|
||||
|
||||
UIView *panView = context.containerView.panContainerView ?: fromVC.view;
|
||||
self.presentingVCTransitionContext = [[HWPresentingVCTransitionContext alloc] initWithFromVC:fromVC toVC:toVC duration:[presentable transitionDuration] containerView:context.containerView];
|
||||
|
||||
// user toggle pan gesture to dismiss.
|
||||
if ([context isInteractive]) {
|
||||
[self interactionDismiss:context fromVC:fromVC toVC:toVC presentable:presentable panView:panView];
|
||||
} else {
|
||||
[self springDismiss:context fromVC:fromVC toVC:toVC presentable:presentable panView:panView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)springDismiss:(id <UIViewControllerContextTransitioning>)context fromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC presentable:(UIViewController <HWPanModalPresentable> *)presentable panView:(UIView *)panView {
|
||||
CGFloat offsetY = 0;
|
||||
HWPanModalShadow *shadowConfig = [presentable contentShadow];
|
||||
if (shadowConfig.shadowColor) {
|
||||
// we should make the panView move further to hide the shadow effect.
|
||||
offsetY = offsetY + shadowConfig.shadowRadius + shadowConfig.shadowOffset.height;
|
||||
if ([presentable showDragIndicator]) {
|
||||
offsetY += [presentable customIndicatorView] ? [presentable customIndicatorView].indicatorSize.height : 13;
|
||||
}
|
||||
}
|
||||
|
||||
[HWPanModalAnimator dismissAnimate:^{
|
||||
[self dismissAnimationForPresentingVC:presentable];
|
||||
panView.hw_top = (context.containerView.frame.size.height + offsetY);
|
||||
} config:presentable completion:^(BOOL completion) {
|
||||
[fromVC.view removeFromSuperview];
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
[self endAppearanceTransitionForController:fromVC];
|
||||
[toVC endAppearanceTransition];
|
||||
}
|
||||
|
||||
[context completeTransition:completion];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)interactionDismiss:(id <UIViewControllerContextTransitioning>)context fromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC presentable:(UIViewController <HWPanModalPresentable> *)presentable panView:(UIView *)panView {
|
||||
[HWPanModalAnimator smoothAnimate:^{
|
||||
if (self.interactiveMode == PanModalInteractiveModeSideslip) {
|
||||
panView.hw_left = panView.hw_width;
|
||||
}
|
||||
|
||||
[self dismissAnimationForPresentingVC:presentable];
|
||||
} duration:[presentable dismissalDuration] completion:^(BOOL completion) {
|
||||
// 因为会有手势交互,所以需要判断transitions是否cancel
|
||||
BOOL finished = ![context transitionWasCancelled];
|
||||
|
||||
if (finished) {
|
||||
[fromVC.view removeFromSuperview];
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
[self endAppearanceTransitionForController:fromVC];
|
||||
[toVC endAppearanceTransition];
|
||||
}
|
||||
|
||||
context.containerView.userInteractionEnabled = YES;
|
||||
}
|
||||
[context completeTransition:finished];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - presenting VC animation
|
||||
|
||||
- (void)presentAnimationForPresentingVC:(UIViewController<HWPanModalPresentable> *)presentable {
|
||||
id<HWPresentingViewControllerAnimatedTransitioning> presentingAnimation = [self presentingVCAnimation:presentable];
|
||||
if (presentingAnimation) {
|
||||
[presentingAnimation presentAnimateTransition:self.presentingVCTransitionContext];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dismissAnimationForPresentingVC:(UIViewController<HWPanModalPresentable> *)presentable {
|
||||
id<HWPresentingViewControllerAnimatedTransitioning> presentingAnimation = [self presentingVCAnimation:presentable];
|
||||
if (presentingAnimation) {
|
||||
[presentingAnimation dismissAnimateTransition:self.presentingVCTransitionContext];
|
||||
}
|
||||
}
|
||||
|
||||
- (UIViewController <HWPanModalPresentable> *)panModalViewController:(id <UIViewControllerContextTransitioning>)context {
|
||||
switch (self.transitionStyle) {
|
||||
case TransitionStylePresentation: {
|
||||
UIViewController *controller = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
if ([controller conformsToProtocol:@protocol(HWPanModalPresentable)]) {
|
||||
return (UIViewController <HWPanModalPresentable> *) controller;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
case TransitionStyleDismissal: {
|
||||
UIViewController *controller = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
if ([controller conformsToProtocol:@protocol(HWPanModalPresentable)]) {
|
||||
return (UIViewController <HWPanModalPresentable> *) controller;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIViewControllerAnimatedTransitioning
|
||||
|
||||
- (void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
|
||||
switch (self.transitionStyle) {
|
||||
case TransitionStylePresentation: {
|
||||
[self animatePresentation:transitionContext];
|
||||
}
|
||||
break;
|
||||
case TransitionStyleDismissal: {
|
||||
[self animateDismissal:transitionContext];
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext {
|
||||
if (transitionContext && [self panModalViewController:transitionContext]) {
|
||||
UIViewController<HWPanModalPresentable> *controller = [self panModalViewController:transitionContext];
|
||||
return [controller transitionDuration];
|
||||
}
|
||||
return kTransitionDuration;
|
||||
}
|
||||
|
||||
#pragma mark - presenting animated transition
|
||||
|
||||
- (id<HWPresentingViewControllerAnimatedTransitioning>)presentingVCAnimation:(UIViewController<HWPanModalPresentable> *)presentable {
|
||||
switch ([presentable presentingVCAnimationStyle]) {
|
||||
case PresentingViewControllerAnimationStylePageSheet:
|
||||
return [HWPageSheetPresentingAnimation new];
|
||||
case PresentingViewControllerAnimationStyleShoppingCart:
|
||||
return [HWShoppingCartPresentingAnimation new];
|
||||
case PresentingViewControllerAnimationStyleCustom:
|
||||
return [presentable customPresentingVCAnimation];
|
||||
default:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
- (void)beginAppearanceTransitionForController:(UIViewController *)viewController isAppearing:(BOOL)isAppearing animated:(BOOL)animated {
|
||||
// Fix `The unbalanced calls to begin/end appearance transitions` warning.
|
||||
if (![viewController isKindOfClass:UINavigationController.class]) {
|
||||
[viewController beginAppearanceTransition:isAppearing animated:animated];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)endAppearanceTransitionForController:(UIViewController *)viewController {
|
||||
if (![viewController isKindOfClass:UINavigationController.class]) {
|
||||
[viewController endAppearanceTransition];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPresentingVCTransitionContext
|
||||
|
||||
- (instancetype)initWithFromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC duration:(NSTimeInterval)duration containerView:(UIView *)containerView {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_fromVC = fromVC;
|
||||
_toVC = toVC;
|
||||
_duration = duration;
|
||||
_containerView = containerView;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (__kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key {
|
||||
if ([key isEqualToString:UITransitionContextFromViewControllerKey]) {
|
||||
return self.fromVC;
|
||||
} else if ([key isEqualToString:UITransitionContextToViewControllerKey]) {
|
||||
return self.toVC;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)transitionDuration {
|
||||
return self.duration;
|
||||
}
|
||||
|
||||
@end
|
||||
51
Pods/HWPanModal/Sources/Animator/HWPresentingVCAnimatedTransitioning.h
generated
Normal file
51
Pods/HWPanModal/Sources/Animator/HWPresentingVCAnimatedTransitioning.h
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// HWCustomPresentingVCAnimatedTransitioning.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/6/12.
|
||||
//
|
||||
|
||||
#ifndef HWCustomPresentingVCAnimatedTransitioning_h
|
||||
#define HWCustomPresentingVCAnimatedTransitioning_h
|
||||
|
||||
NS_SWIFT_NAME(PanModalPresentingViewControllerContextTransitioning)
|
||||
@protocol HWPresentingViewControllerContextTransitioning <NSObject>
|
||||
|
||||
/**
|
||||
* Returns a view controller involved in the transition.
|
||||
* @return The view controller object for the specified key or nil if the view controller could not be found.
|
||||
*/
|
||||
- (__kindof UIViewController * _Nullable )viewControllerForKey:(nonnull UITransitionContextViewControllerKey)key;
|
||||
|
||||
/**
|
||||
* The Animation duration gets from ViewController which conforms HWPanModalPresentable
|
||||
* - (NSTimeInterval)transitionDuration;
|
||||
*/
|
||||
- (NSTimeInterval)transitionDuration;
|
||||
|
||||
/**
|
||||
* Transition container, from UIViewControllerContextTransitioning protocol
|
||||
*/
|
||||
@property(nonnull, nonatomic, readonly) UIView *containerView;
|
||||
|
||||
@end
|
||||
|
||||
NS_SWIFT_NAME(PanModalPresentingViewControllerAnimatedTransitioning)
|
||||
@protocol HWPresentingViewControllerAnimatedTransitioning <NSObject>
|
||||
|
||||
/**
|
||||
* Write you custom animation when present.
|
||||
*/
|
||||
- (void)presentAnimateTransition:(nonnull id<HWPresentingViewControllerContextTransitioning>)context NS_SWIFT_NAME(presentTransition(context:));
|
||||
/**
|
||||
* Write you custom animation when dismiss.
|
||||
*/
|
||||
- (void)dismissAnimateTransition:(nonnull id<HWPresentingViewControllerContextTransitioning>)context NS_SWIFT_NAME(dismissTransition(context:));
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#endif /* HWCustomPresentingVCAnimatedTransitioning_h */
|
||||
|
||||
|
||||
|
||||
17
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWPageSheetPresentingAnimation.h
generated
Normal file
17
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWPageSheetPresentingAnimation.h
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// HWPageSheetPresentingAnimation.h
|
||||
// HWPanModal-iOS10.0
|
||||
//
|
||||
// Created by heath wang on 2019/9/5.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HWPresentingVCAnimatedTransitioning.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPageSheetPresentingAnimation : NSObject <HWPresentingViewControllerAnimatedTransitioning>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
29
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWPageSheetPresentingAnimation.m
generated
Normal file
29
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWPageSheetPresentingAnimation.m
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// HWPageSheetPresentingAnimation.m
|
||||
// HWPanModal-iOS10.0
|
||||
//
|
||||
// Created by heath wang on 2019/9/5.
|
||||
//
|
||||
|
||||
#import "HWPageSheetPresentingAnimation.h"
|
||||
|
||||
@implementation HWPageSheetPresentingAnimation
|
||||
|
||||
- (void)presentAnimateTransition:(nonnull id <HWPresentingViewControllerContextTransitioning>)context {
|
||||
NSTimeInterval duration = [context transitionDuration];
|
||||
UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.9 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
||||
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
|
||||
CGFloat scale = 1 - statusBarHeight * 2 / CGRectGetHeight(fromVC.view.bounds);
|
||||
fromVC.view.transform = CGAffineTransformMakeScale(scale, scale);
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAnimateTransition:(nonnull id <HWPresentingViewControllerContextTransitioning>)context {
|
||||
UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
toVC.view.transform = CGAffineTransformIdentity;
|
||||
}
|
||||
|
||||
@end
|
||||
17
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWShoppingCartPresentingAnimation.h
generated
Normal file
17
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWShoppingCartPresentingAnimation.h
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// HWShoppingCartPresentingAnimation.h
|
||||
// HWPanModal-iOS10.0
|
||||
//
|
||||
// Created by heath wang on 2019/9/5.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HWPresentingVCAnimatedTransitioning.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWShoppingCartPresentingAnimation : NSObject <HWPresentingViewControllerAnimatedTransitioning>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
39
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWShoppingCartPresentingAnimation.m
generated
Normal file
39
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWShoppingCartPresentingAnimation.m
generated
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// HWShoppingCartPresentingAnimation.m
|
||||
// HWPanModal-iOS10.0
|
||||
//
|
||||
// Created by heath wang on 2019/9/5.
|
||||
//
|
||||
|
||||
#import "HWShoppingCartPresentingAnimation.h"
|
||||
|
||||
@implementation HWShoppingCartPresentingAnimation
|
||||
|
||||
- (void)presentAnimateTransition:(nonnull id <HWPresentingViewControllerContextTransitioning>)context {
|
||||
|
||||
NSTimeInterval duration = [context transitionDuration];
|
||||
UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
|
||||
CGFloat scale = 1 - statusBarHeight * 2 / CGRectGetHeight(fromVC.view.bounds);
|
||||
[UIView animateWithDuration:duration * 0.4 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
|
||||
CATransform3D tran = CATransform3DIdentity;
|
||||
tran.m34 = -1 / 1000.0f;
|
||||
tran = CATransform3DRotate(tran, M_PI / 16, 1, 0, 0);
|
||||
tran = CATransform3DTranslate(tran, 0, 0, -100);
|
||||
fromVC.view.layer.transform = tran;
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
[UIView animateWithDuration:duration * 0.6 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
|
||||
fromVC.view.layer.transform = CATransform3DMakeScale(scale, scale, 1);
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAnimateTransition:(nonnull id <HWPresentingViewControllerContextTransitioning>)context {
|
||||
UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
toVC.view.layer.transform = CATransform3DIdentity;
|
||||
}
|
||||
|
||||
@end
|
||||
18
Pods/HWPanModal/Sources/Category/UIScrollView+Helper.h
generated
Normal file
18
Pods/HWPanModal/Sources/Category/UIScrollView+Helper.h
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// UIScrollView+Helper.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/10/15.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIScrollView (Helper)
|
||||
|
||||
@property (nonatomic, assign, readonly) BOOL isScrolling;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
16
Pods/HWPanModal/Sources/Category/UIScrollView+Helper.m
generated
Normal file
16
Pods/HWPanModal/Sources/Category/UIScrollView+Helper.m
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// UIScrollView+Helper.m
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/10/15.
|
||||
//
|
||||
|
||||
#import "UIScrollView+Helper.h"
|
||||
|
||||
@implementation UIScrollView (Helper)
|
||||
|
||||
- (BOOL)isScrolling {
|
||||
return (self.isDragging && !self.isDecelerating) || self.isTracking;
|
||||
}
|
||||
|
||||
@end
|
||||
28
Pods/HWPanModal/Sources/Category/UIView+HW_Frame.h
generated
Normal file
28
Pods/HWPanModal/Sources/Category/UIView+HW_Frame.h
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// UIView+HW_Frame.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/20.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIView (HW_Frame)
|
||||
|
||||
@property (nonatomic, assign) CGFloat hw_left; ///< Shortcut for frame.origin.x.
|
||||
@property (nonatomic, assign) CGFloat hw_top; ///< Shortcut for frame.origin.y
|
||||
@property (nonatomic, assign) CGFloat hw_right; ///< Shortcut for frame.origin.x + frame.size.width
|
||||
@property (nonatomic, assign) CGFloat hw_bottom; ///< Shortcut for frame.origin.y + frame.size.height
|
||||
@property (nonatomic, assign) CGFloat hw_width; ///< Shortcut for frame.size.width.
|
||||
@property (nonatomic, assign) CGFloat hw_height; ///< Shortcut for frame.size.height.
|
||||
@property (nonatomic, assign) CGFloat hw_centerX; ///< Shortcut for center.x
|
||||
@property (nonatomic, assign) CGFloat hw_centerY; ///< Shortcut for center.y
|
||||
@property (nonatomic, assign) CGPoint hw_origin; ///< Shortcut for frame.origin.
|
||||
@property (nonatomic, assign) CGSize hw_size; ///< Shortcut for frame.size.
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
109
Pods/HWPanModal/Sources/Category/UIView+HW_Frame.m
generated
Normal file
109
Pods/HWPanModal/Sources/Category/UIView+HW_Frame.m
generated
Normal file
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// UIView+HW_Frame.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/20.
|
||||
//
|
||||
|
||||
#import "UIView+HW_Frame.h"
|
||||
|
||||
@implementation UIView (HW_Frame)
|
||||
|
||||
- (CGFloat)hw_left {
|
||||
return self.frame.origin.x;
|
||||
}
|
||||
|
||||
- (void)setHw_left:(CGFloat)hwLeft {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin.x = hwLeft;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_top {
|
||||
return self.frame.origin.y;
|
||||
}
|
||||
|
||||
- (void)setHw_top:(CGFloat)hwTop {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin.y = hwTop;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_right {
|
||||
return self.frame.origin.x + self.frame.size.width;
|
||||
}
|
||||
|
||||
- (void)setHw_right:(CGFloat)hwRight {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin.x = hwRight - self.frame.size.width;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_bottom {
|
||||
return self.frame.origin.y + self.frame.size.height;
|
||||
}
|
||||
|
||||
- (void)setHw_bottom:(CGFloat)hwBottom {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin.y = hwBottom - self.frame.size.height;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_width {
|
||||
return self.frame.size.width;
|
||||
}
|
||||
|
||||
- (void)setHw_width:(CGFloat)hwWidth {
|
||||
CGRect frame = self.frame;
|
||||
frame.size.width = hwWidth;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_height {
|
||||
return self.frame.size.height;
|
||||
}
|
||||
|
||||
- (void)setHw_height:(CGFloat)hwHeight {
|
||||
CGRect frame = self.frame;
|
||||
frame.size.height = hwHeight;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_centerX {
|
||||
return self.center.x;
|
||||
}
|
||||
|
||||
- (void)setHw_centerX:(CGFloat)hwCenterX {
|
||||
self.center = CGPointMake(hwCenterX, self.center.y);
|
||||
}
|
||||
|
||||
- (CGFloat)hw_centerY {
|
||||
return self.center.y;
|
||||
}
|
||||
|
||||
- (void)setHw_centerY:(CGFloat)hwCenterY {
|
||||
self.center = CGPointMake(self.center.x, hwCenterY);
|
||||
}
|
||||
|
||||
- (CGPoint)hw_origin {
|
||||
return self.frame.origin;
|
||||
}
|
||||
|
||||
- (void)setHw_origin:(CGPoint)hwOrigin {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin = hwOrigin;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGSize)hw_size {
|
||||
return self.frame.size;
|
||||
}
|
||||
|
||||
- (void)setHw_size:(CGSize)hwSize {
|
||||
CGRect frame = self.frame;
|
||||
frame.size = hwSize;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
32
Pods/HWPanModal/Sources/Controller/HWPanModalPresentationController.h
generated
Normal file
32
Pods/HWPanModal/Sources/Controller/HWPanModalPresentationController.h
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// HWPanModalPresentationController.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
@class HWDimmedView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
@interface HWPanModalPresentationController : UIPresentationController
|
||||
|
||||
@property (nonatomic, readonly) HWDimmedView *backgroundView;
|
||||
@property (nonatomic, readonly) PresentationState currentPresentationState;
|
||||
|
||||
- (void)setNeedsLayoutUpdate;
|
||||
|
||||
- (void)updateUserHitBehavior;
|
||||
|
||||
- (void)transitionToState:(PresentationState)state animated:(BOOL)animated;
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated;
|
||||
|
||||
- (void)dismissAnimated:(BOOL)animated completion:(void (^)(void))completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
599
Pods/HWPanModal/Sources/Controller/HWPanModalPresentationController.m
generated
Normal file
599
Pods/HWPanModal/Sources/Controller/HWPanModalPresentationController.m
generated
Normal file
@@ -0,0 +1,599 @@
|
||||
//
|
||||
// HWPanModalPresentationController.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import "HWPanModalPresentationController.h"
|
||||
#import "HWDimmedView.h"
|
||||
#import "HWPanContainerView.h"
|
||||
#import "UIViewController+LayoutHelper.h"
|
||||
#import "HWPanModalAnimator.h"
|
||||
#import "HWPanModalInteractiveAnimator.h"
|
||||
#import "HWPanModalPresentationDelegate.h"
|
||||
#import "UIViewController+PanModalPresenter.h"
|
||||
#import "HWPanIndicatorView.h"
|
||||
#import "UIView+HW_Frame.h"
|
||||
#import "HWPanModalPresentableHandler.h"
|
||||
|
||||
@interface HWPanModalPresentationController () <UIGestureRecognizerDelegate, HWPanModalPresentableHandlerDelegate, HWPanModalPresentableHandlerDataSource>
|
||||
|
||||
// 判断弹出的view是否在做动画
|
||||
@property (nonatomic, assign) BOOL isPresentedViewAnimating;
|
||||
@property (nonatomic, assign) PresentationState currentPresentationState;
|
||||
|
||||
@property (nonatomic, strong) id<HWPanModalPresentable> presentable;
|
||||
|
||||
// view
|
||||
@property (nonatomic, strong) HWDimmedView *backgroundView;
|
||||
@property (nonatomic, strong) HWPanContainerView *panContainerView;
|
||||
@property (nonatomic, strong) UIView<HWPanModalIndicatorProtocol> *dragIndicatorView;
|
||||
|
||||
@property (nonatomic, strong) HWPanModalPresentableHandler *handler;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalPresentationController
|
||||
|
||||
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(nullable UIViewController *)presentingViewController {
|
||||
self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController];
|
||||
if (self) {
|
||||
_handler = [[HWPanModalPresentableHandler alloc] initWithPresentable:[self presentable]];
|
||||
_handler.delegate = self;
|
||||
_handler.dataSource = self;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - overridden
|
||||
|
||||
- (UIView *)presentedView {
|
||||
return self.panContainerView;
|
||||
}
|
||||
|
||||
- (void)containerViewWillLayoutSubviews {
|
||||
[super containerViewWillLayoutSubviews];
|
||||
[self configureViewLayout];
|
||||
}
|
||||
|
||||
#pragma mark - Tracking the Transition Start and End
|
||||
|
||||
- (void)presentationTransitionWillBegin {
|
||||
[[self presentable] panModalTransitionWillBegin];
|
||||
|
||||
if (!self.containerView)
|
||||
return;
|
||||
|
||||
[self layoutBackgroundView:self.containerView];
|
||||
|
||||
if ([[self presentable] originPresentationState] == PresentationStateLong) {
|
||||
self.currentPresentationState = PresentationStateLong;
|
||||
} else if ([[self presentable] originPresentationState] == PresentationStateMedium) {
|
||||
self.currentPresentationState = PresentationStateMedium;
|
||||
}
|
||||
|
||||
[self layoutPresentedView:self.containerView];
|
||||
[self.handler configureScrollViewInsets];
|
||||
|
||||
if (!self.presentedViewController.transitionCoordinator) {
|
||||
self.backgroundView.dimState = DimStateMax;
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) wkSelf = self;
|
||||
__block BOOL isAnimated = NO;
|
||||
[self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
wkSelf.backgroundView.dimState = DimStateMax;
|
||||
[wkSelf.presentedViewController setNeedsStatusBarAppearanceUpdate];
|
||||
isAnimated = YES;
|
||||
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
||||
if (!isAnimated) {
|
||||
/// In some cases, for example, present a `hw` when a navigation controller is pushing a new vc, `animateAlongsideTransition` will not call.
|
||||
/// If not called, call it here.
|
||||
wkSelf.backgroundView.dimState = DimStateMax;
|
||||
[wkSelf.presentedViewController setNeedsStatusBarAppearanceUpdate];
|
||||
}
|
||||
if ([[wkSelf presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
// hack TransitionView
|
||||
[wkSelf.containerView setValue:@(YES) forKey:@"ignoreDirectTouchEvents"];
|
||||
}
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
- (void)presentationTransitionDidEnd:(BOOL)completed {
|
||||
[[self presentable] panModalTransitionDidFinish];
|
||||
if (completed)
|
||||
return;
|
||||
|
||||
[self.backgroundView removeFromSuperview];
|
||||
[self.presentedView endEditing:YES];
|
||||
}
|
||||
|
||||
- (void)dismissalTransitionWillBegin {
|
||||
id <UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentedViewController.transitionCoordinator;
|
||||
if (!transitionCoordinator) {
|
||||
self.backgroundView.dimState = DimStateOff;
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) wkSelf = self;
|
||||
[transitionCoordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
wkSelf.dragIndicatorView.alpha = 0;
|
||||
wkSelf.backgroundView.dimState = DimStateOff;
|
||||
[wkSelf.presentedViewController setNeedsStatusBarAppearanceUpdate];
|
||||
} completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissalTransitionDidEnd:(BOOL)completed {
|
||||
if (completed) {
|
||||
// break the delegate
|
||||
self.delegate = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIContentContainer protocol
|
||||
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
|
||||
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||
|
||||
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
if (self && [self presentable]) {
|
||||
[self adjustPresentedViewFrame];
|
||||
|
||||
if ([self.presentable shouldRoundTopCorners]) {
|
||||
[self addRoundedCornersToView:self.panContainerView.contentView];
|
||||
}
|
||||
[self updateDragIndicatorView];
|
||||
}
|
||||
} completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
[self transitionToState:self.currentPresentationState animated:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - public method
|
||||
|
||||
- (void)setNeedsLayoutUpdate {
|
||||
[self configureViewLayout];
|
||||
[self adjustPresentedViewFrame];
|
||||
|
||||
[self updateBackgroundColor];
|
||||
[self updateContainerViewShadow];
|
||||
[self updateDragIndicatorView];
|
||||
[self updateRoundedCorners];
|
||||
|
||||
[self.handler observeScrollable];
|
||||
[self.handler configureScrollViewInsets];
|
||||
[self checkEdgeInteractive];
|
||||
}
|
||||
|
||||
- (void)transitionToState:(PresentationState)state animated:(BOOL)animated {
|
||||
|
||||
if (![self.presentable shouldTransitionToState:state])
|
||||
return;
|
||||
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
[self.presentable willTransitionToState:state];
|
||||
|
||||
switch (state) {
|
||||
case PresentationStateLong: {
|
||||
[self snapToYPos:self.handler.longFormYPosition animated:animated];
|
||||
}
|
||||
break;
|
||||
case PresentationStateMedium: {
|
||||
[self snapToYPos:self.handler.mediumFormYPosition animated:animated];
|
||||
}
|
||||
break;
|
||||
case PresentationStateShort: {
|
||||
[self snapToYPos:self.handler.shortFormYPosition animated:animated];
|
||||
}
|
||||
break;
|
||||
}
|
||||
self.currentPresentationState = state;
|
||||
[[self presentable] didChangeTransitionToState:state];
|
||||
}
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated {
|
||||
[self.handler setScrollableContentOffset:offset animated:animated];
|
||||
}
|
||||
|
||||
- (void)updateUserHitBehavior {
|
||||
[self checkVCContainerEventPass];
|
||||
[self checkBackgroundViewEventPass];
|
||||
}
|
||||
|
||||
#pragma mark - layout
|
||||
|
||||
- (void)adjustPresentedViewFrame {
|
||||
|
||||
if (!self.containerView)
|
||||
return;
|
||||
|
||||
CGRect frame = self.containerView.frame;
|
||||
CGSize size = CGSizeMake(CGRectGetWidth(frame), CGRectGetHeight(frame) - self.handler.anchoredYPosition);
|
||||
|
||||
self.presentedView.hw_size = frame.size;
|
||||
self.panContainerView.contentView.frame = CGRectMake(0, 0, size.width, size.height);
|
||||
self.presentedViewController.view.frame = self.panContainerView.contentView.bounds;
|
||||
[self.presentedViewController.view setNeedsLayout];
|
||||
[self.presentedViewController.view layoutIfNeeded];
|
||||
}
|
||||
|
||||
/**
|
||||
* add backGroundView并设置约束
|
||||
*/
|
||||
- (void)layoutBackgroundView:(UIView *)containerView {
|
||||
[containerView addSubview:self.backgroundView];
|
||||
[self updateBackgroundColor];
|
||||
self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
NSArray *hCons = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
|
||||
NSArray *vCons = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
|
||||
[NSLayoutConstraint activateConstraints:hCons];
|
||||
[NSLayoutConstraint activateConstraints:vCons];
|
||||
}
|
||||
|
||||
- (void)updateBackgroundColor {
|
||||
self.backgroundView.blurTintColor = [self.presentable backgroundConfig].blurTintColor;
|
||||
}
|
||||
|
||||
- (void)layoutPresentedView:(UIView *)containerView {
|
||||
if (!self.presentable)
|
||||
return;
|
||||
|
||||
self.handler.presentedView = self.presentedView;
|
||||
|
||||
[containerView addSubview:self.presentedView];
|
||||
[containerView addGestureRecognizer:self.handler.panGestureRecognizer];
|
||||
|
||||
if ([self.presentable allowScreenEdgeInteractive]) {
|
||||
[containerView addGestureRecognizer:self.handler.screenEdgeGestureRecognizer];
|
||||
[self.handler.screenEdgeGestureRecognizer addTarget:self action:@selector(screenEdgeInteractiveAction:)];
|
||||
}
|
||||
|
||||
[self setNeedsLayoutUpdate];
|
||||
[self adjustPanContainerBackgroundColor];
|
||||
|
||||
[[self presentable] presentedViewDidMoveToSuperView];
|
||||
}
|
||||
|
||||
- (void)adjustPanContainerBackgroundColor {
|
||||
self.panContainerView.contentView.backgroundColor = self.presentedViewController.view.backgroundColor ? : [self.presentable panScrollable].backgroundColor;
|
||||
}
|
||||
|
||||
- (void)updateDragIndicatorView {
|
||||
if ([self.presentable showDragIndicator]) {
|
||||
[self addDragIndicatorViewToView:self.panContainerView];
|
||||
} else {
|
||||
self.dragIndicatorView.hidden = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addDragIndicatorViewToView:(UIView *)view {
|
||||
// if has been add, won't update it.
|
||||
self.dragIndicatorView.hidden = NO;
|
||||
|
||||
CGSize indicatorSize = [self.dragIndicatorView indicatorSize];
|
||||
|
||||
if (self.dragIndicatorView.superview == view) {
|
||||
self.dragIndicatorView.frame = CGRectMake((view.hw_width - indicatorSize.width) / 2, -kIndicatorYOffset - indicatorSize.height, indicatorSize.width, indicatorSize.height);
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
return;
|
||||
}
|
||||
|
||||
self.handler.dragIndicatorView = self.dragIndicatorView;
|
||||
[view addSubview:self.dragIndicatorView];
|
||||
|
||||
self.dragIndicatorView.frame = CGRectMake((view.hw_width - indicatorSize.width) / 2, -kIndicatorYOffset - indicatorSize.height, indicatorSize.width, indicatorSize.height);
|
||||
[self.dragIndicatorView setupSubviews];
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
}
|
||||
|
||||
- (void)updateRoundedCorners {
|
||||
if ([self.presentable shouldRoundTopCorners]) {
|
||||
[self addRoundedCornersToView:self.panContainerView.contentView];
|
||||
} else {
|
||||
[self resetRoundedCornersToView:self.panContainerView.contentView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addRoundedCornersToView:(UIView *)view {
|
||||
CGFloat radius = [self.presentable cornerRadius];
|
||||
|
||||
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopRight | UIRectCornerTopLeft cornerRadii:CGSizeMake(radius, radius)];
|
||||
|
||||
CAShapeLayer *mask = [CAShapeLayer new];
|
||||
mask.path = bezierPath.CGPath;
|
||||
view.layer.mask = mask;
|
||||
|
||||
// 提高性能
|
||||
view.layer.shouldRasterize = YES;
|
||||
view.layer.rasterizationScale = [UIScreen mainScreen].scale;
|
||||
}
|
||||
|
||||
- (void)resetRoundedCornersToView:(UIView *)view {
|
||||
view.layer.mask = nil;
|
||||
view.layer.shouldRasterize = NO;
|
||||
}
|
||||
|
||||
- (void)updateContainerViewShadow {
|
||||
HWPanModalShadow *shadow = [[self presentable] contentShadow];
|
||||
if (shadow.shadowColor) {
|
||||
[self.panContainerView updateShadow:shadow.shadowColor shadowRadius:shadow.shadowRadius shadowOffset:shadow.shadowOffset shadowOpacity:shadow.shadowOpacity];
|
||||
} else {
|
||||
[self.panContainerView clearShadow];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates & stores the layout anchor points & options
|
||||
*/
|
||||
- (void)configureViewLayout {
|
||||
|
||||
[self.handler configureViewLayout];
|
||||
self.containerView.userInteractionEnabled = [[self presentable] isUserInteractionEnabled];
|
||||
}
|
||||
|
||||
#pragma mark - event passing through
|
||||
|
||||
- (void)checkVCContainerEventPass {
|
||||
BOOL eventPassValue = [[self presentable] allowsTouchEventsPassingThroughTransitionView];
|
||||
// hack TransitionView
|
||||
[self.containerView setValue:@(eventPassValue) forKey:@"ignoreDirectTouchEvents"];
|
||||
}
|
||||
|
||||
- (void)checkBackgroundViewEventPass {
|
||||
if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
self.backgroundView.userInteractionEnabled = NO;
|
||||
self.backgroundView.tapBlock = nil;
|
||||
} else {
|
||||
self.backgroundView.userInteractionEnabled = YES;
|
||||
__weak typeof(self) wkSelf = self;
|
||||
self.backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
|
||||
if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
|
||||
[wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - y position update
|
||||
|
||||
- (void)snapToYPos:(CGFloat)yPos animated:(BOOL)animated {
|
||||
|
||||
if (animated) {
|
||||
[HWPanModalAnimator animate:^{
|
||||
self.isPresentedViewAnimating = YES;
|
||||
[self adjustToYPos:yPos];
|
||||
} config:self.presentable completion:^(BOOL completion) {
|
||||
self.isPresentedViewAnimating = NO;
|
||||
}];
|
||||
} else {
|
||||
[self adjustToYPos:yPos];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)adjustToYPos:(CGFloat)yPos {
|
||||
self.presentedView.hw_top = MAX(yPos, self.handler.anchoredYPosition);
|
||||
|
||||
// change dim background starting from shortFormYPosition.
|
||||
if (self.presentedView.frame.origin.y >= self.handler.shortFormYPosition) {
|
||||
|
||||
CGFloat yDistanceFromShortForm = self.presentedView.frame.origin.y - self.handler.shortFormYPosition;
|
||||
CGFloat bottomHeight = self.containerView.hw_height - self.handler.shortFormYPosition;
|
||||
CGFloat percent = yDistanceFromShortForm / bottomHeight;
|
||||
self.backgroundView.dimState = DimStatePercent;
|
||||
self.backgroundView.percent = 1 - percent;
|
||||
|
||||
[self.presentable panModalGestureRecognizer:self.handler.panGestureRecognizer dismissPercent:MIN(percent, 1)];
|
||||
if (self.presentedViewController.isBeingDismissed) {
|
||||
[[self interactiveAnimator] updateInteractiveTransition:MIN(percent, 1)];
|
||||
}
|
||||
} else {
|
||||
self.backgroundView.dimState = DimStateMax;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentableHandlerDelegate
|
||||
|
||||
- (void)adjustPresentableYPos:(CGFloat)yPos {
|
||||
[self adjustToYPos:yPos];
|
||||
}
|
||||
|
||||
- (void)dismiss:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode {
|
||||
[self dismiss:isInteractive mode:mode animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)dismiss:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode animated:(BOOL)animated completion:(void (^)(void))completion {
|
||||
self.presentedViewController.hw_panModalPresentationDelegate.interactive = isInteractive;
|
||||
self.presentedViewController.hw_panModalPresentationDelegate.interactiveMode = mode;
|
||||
[self.presentable panModalWillDismiss];
|
||||
[self.presentedViewController dismissViewControllerAnimated:animated completion:^{
|
||||
if (completion) completion();
|
||||
[self.presentable panModalDidDismissed];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated:(BOOL)animated completion:(nonnull void (^)(void))completion {
|
||||
[self dismiss:NO mode:PanModalInteractiveModeNone animated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (void)presentableTransitionToState:(PresentationState)state {
|
||||
[self transitionToState:state animated:YES];
|
||||
}
|
||||
|
||||
- (PresentationState)getCurrentPresentationState {
|
||||
return self.currentPresentationState;
|
||||
}
|
||||
|
||||
#pragma mark - interactive handle
|
||||
|
||||
- (void)finishInteractiveTransition {
|
||||
if (self.presentedViewController.isBeingDismissed) {
|
||||
// make the containerView can not response event action.
|
||||
self.containerView.userInteractionEnabled = NO;
|
||||
[[self interactiveAnimator] finishInteractiveTransition];
|
||||
|
||||
if (self.presentedViewController.hw_panModalPresentationDelegate.interactiveMode != PanModalInteractiveModeDragDown)
|
||||
return;
|
||||
|
||||
if ([[self presentable] presentingVCAnimationStyle] > PresentingViewControllerAnimationStyleNone) {
|
||||
[HWPanModalAnimator animate:^{
|
||||
[self presentedView].hw_top = self.containerView.frame.size.height;
|
||||
self.dragIndicatorView.alpha = 0;
|
||||
self.backgroundView.dimState = DimStateOff;
|
||||
} config:[self presentable] completion:^(BOOL completion) {
|
||||
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelInteractiveTransition {
|
||||
if (self.presentedViewController.isBeingDismissed) {
|
||||
[[self interactiveAnimator] cancelInteractiveTransition];
|
||||
self.presentedViewController.hw_panModalPresentationDelegate.interactiveMode = PanModalInteractiveModeNone;
|
||||
self.presentedViewController.hw_panModalPresentationDelegate.interactive = NO;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentableHandlerDataSource
|
||||
|
||||
- (CGSize)containerSize {
|
||||
return self.containerView.bounds.size;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingDismissed {
|
||||
return self.presentedViewController.isBeingDismissed;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingPresented {
|
||||
return self.presentedViewController.isBeingPresented;
|
||||
}
|
||||
|
||||
- (BOOL)isPresentedViewAnchored {
|
||||
|
||||
if (![[self presentable] shouldRespondToPanModalGestureRecognizer:self.handler.panGestureRecognizer]) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (!self.isPresentedViewAnimating && self.handler.extendsPanScrolling && (CGRectGetMinY(self.presentedView.frame) <= self.handler.anchoredYPosition || HW_TWO_FLOAT_IS_EQUAL(CGRectGetMinY(self.presentedView.frame), self.handler.anchoredYPosition))) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isPresentedControllerInteractive {
|
||||
return self.presentedViewController.hw_panModalPresentationDelegate.interactive;
|
||||
}
|
||||
|
||||
- (BOOL)isFormPositionAnimating {
|
||||
return self.isPresentedViewAnimating;
|
||||
}
|
||||
|
||||
#pragma mark - Screen Gesture enevt
|
||||
|
||||
- (void)screenEdgeInteractiveAction:(UIPanGestureRecognizer *)recognizer {
|
||||
CGPoint translation = [recognizer translationInView:recognizer.view];
|
||||
CGFloat percent = translation.x / CGRectGetWidth(recognizer.view.bounds);
|
||||
CGPoint velocity = [recognizer velocityInView:recognizer.view];
|
||||
|
||||
switch (recognizer.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
[self dismiss:YES mode:PanModalInteractiveModeSideslip];
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateEnded: {
|
||||
if (percent > 0.5 || velocity.x >= [[self presentable] minHorizontalVelocityToTriggerScreenEdgeDismiss]) {
|
||||
[self finishInteractiveTransition];
|
||||
} else {
|
||||
[self cancelInteractiveTransition];
|
||||
}
|
||||
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateChanged: {
|
||||
|
||||
[[self interactiveAnimator] updateInteractiveTransition:percent];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)checkEdgeInteractive {
|
||||
//TODO: changed the user interactive, if someone else has different requirements, change it.
|
||||
self.handler.screenEdgeGestureRecognizer.enabled = [[self presentable] allowScreenEdgeInteractive];
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (id <HWPanModalPresentable>)presentable {
|
||||
if ([self.presentedViewController conformsToProtocol:@protocol(HWPanModalPresentable)]) {
|
||||
return (id <HWPanModalPresentable>) self.presentedViewController;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (HWPanModalInteractiveAnimator *)interactiveAnimator {
|
||||
HWPanModalPresentationDelegate *presentationDelegate = self.presentedViewController.hw_panModalPresentationDelegate;
|
||||
return presentationDelegate.interactiveDismissalAnimator;
|
||||
}
|
||||
|
||||
- (HWDimmedView *)backgroundView {
|
||||
if (!_backgroundView) {
|
||||
if (self.presentable) {
|
||||
_backgroundView = [[HWDimmedView alloc] initWithBackgroundConfig:[self.presentable backgroundConfig]];
|
||||
} else {
|
||||
_backgroundView = [[HWDimmedView alloc] init];
|
||||
}
|
||||
|
||||
if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
_backgroundView.userInteractionEnabled = NO;
|
||||
} else {
|
||||
__weak typeof(self) wkSelf = self;
|
||||
_backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
|
||||
if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
|
||||
[wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return _backgroundView;
|
||||
}
|
||||
|
||||
- (HWPanContainerView *)panContainerView {
|
||||
if (!_panContainerView) {
|
||||
_panContainerView = [[HWPanContainerView alloc] initWithPresentedView:self.presentedViewController.view frame:self.containerView.frame];
|
||||
}
|
||||
|
||||
return _panContainerView;
|
||||
}
|
||||
|
||||
- (UIView<HWPanModalIndicatorProtocol> *)dragIndicatorView {
|
||||
|
||||
if (!_dragIndicatorView) {
|
||||
if ([self presentable] &&
|
||||
[[self presentable] respondsToSelector:@selector(customIndicatorView)] &&
|
||||
[[self presentable] customIndicatorView] != nil) {
|
||||
_dragIndicatorView = [[self presentable] customIndicatorView];
|
||||
// set the indicator size first in case `setupSubviews` can Not get the right size.
|
||||
_dragIndicatorView.hw_size = [[[self presentable] customIndicatorView] indicatorSize];
|
||||
} else {
|
||||
_dragIndicatorView = [HWPanIndicatorView new];
|
||||
}
|
||||
}
|
||||
|
||||
return _dragIndicatorView;
|
||||
}
|
||||
|
||||
@end
|
||||
29
Pods/HWPanModal/Sources/Delegate/HWPanModalPresentationDelegate.h
generated
Normal file
29
Pods/HWPanModal/Sources/Delegate/HWPanModalPresentationDelegate.h
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// HWPanModalPresentationDelegate.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class HWPanModalInteractiveAnimator;
|
||||
|
||||
typedef NS_ENUM(NSInteger, PanModalInteractiveMode) {
|
||||
PanModalInteractiveModeNone,
|
||||
PanModalInteractiveModeSideslip, // 侧滑返回
|
||||
PanModalInteractiveModeDragDown, // 向下拖拽返回
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanModalPresentationDelegate : NSObject <UIViewControllerTransitioningDelegate, UIAdaptivePresentationControllerDelegate, UIPopoverPresentationControllerDelegate>
|
||||
|
||||
@property (nonatomic, assign) BOOL interactive;
|
||||
@property (nonatomic, assign) PanModalInteractiveMode interactiveMode;
|
||||
@property (nonnull, nonatomic, strong, readonly) HWPanModalInteractiveAnimator *interactiveDismissalAnimator;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
66
Pods/HWPanModal/Sources/Delegate/HWPanModalPresentationDelegate.m
generated
Normal file
66
Pods/HWPanModal/Sources/Delegate/HWPanModalPresentationDelegate.m
generated
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// HWPanModalPresentationDelegate.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import "HWPanModalPresentationDelegate.h"
|
||||
#import "HWPanModalPresentationAnimator.h"
|
||||
#import "HWPanModalPresentationController.h"
|
||||
#import "HWPanModalInteractiveAnimator.h"
|
||||
|
||||
@interface HWPanModalPresentationDelegate ()
|
||||
|
||||
@property (nonatomic, strong) HWPanModalInteractiveAnimator *interactiveDismissalAnimator;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalPresentationDelegate
|
||||
|
||||
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
|
||||
return [[HWPanModalPresentationAnimator alloc] initWithTransitionStyle:TransitionStylePresentation interactiveMode:PanModalInteractiveModeNone];
|
||||
}
|
||||
|
||||
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
|
||||
return [[HWPanModalPresentationAnimator alloc] initWithTransitionStyle:TransitionStyleDismissal interactiveMode:self.interactiveMode];
|
||||
}
|
||||
|
||||
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
|
||||
if (self.interactive) {
|
||||
return self.interactiveDismissalAnimator;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source {
|
||||
UIPresentationController *controller = [[HWPanModalPresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
|
||||
controller.delegate = self;
|
||||
return controller;
|
||||
}
|
||||
|
||||
#pragma mark - UIAdaptivePresentationControllerDelegate
|
||||
|
||||
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection {
|
||||
return UIModalPresentationNone;
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (HWPanModalInteractiveAnimator *)interactiveDismissalAnimator {
|
||||
if (!_interactiveDismissalAnimator) {
|
||||
_interactiveDismissalAnimator = [[HWPanModalInteractiveAnimator alloc] init];
|
||||
}
|
||||
return _interactiveDismissalAnimator;
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
- (void)dealloc {
|
||||
NSLog(@"%s", __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
36
Pods/HWPanModal/Sources/HWPanModal.h
generated
Normal file
36
Pods/HWPanModal/Sources/HWPanModal.h
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// HWPanModal.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/4/30.
|
||||
//
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for HWPanModal.
|
||||
FOUNDATION_EXPORT double HWPanModalVersionNumber;
|
||||
|
||||
//! Project version string for JYHitchModule.
|
||||
FOUNDATION_EXPORT const unsigned char HWPanModalVersionString[];
|
||||
|
||||
// protocol
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
#import <HWPanModal/HWPanModalPanGestureDelegate.h>
|
||||
#import <HWPanModal/HWPanModalHeight.h>
|
||||
|
||||
#import <HWPanModal/HWPanModalPresenterProtocol.h>
|
||||
|
||||
// category
|
||||
#import <HWPanModal/UIViewController+PanModalDefault.h>
|
||||
#import <HWPanModal/UIViewController+Presentation.h>
|
||||
#import <HWPanModal/UIViewController+PanModalPresenter.h>
|
||||
|
||||
// custom animation
|
||||
#import <HWPanModal/HWPresentingVCAnimatedTransitioning.h>
|
||||
|
||||
// view
|
||||
#import <HWPanModal/HWPanModalIndicatorProtocol.h>
|
||||
#import <HWPanModal/HWPanIndicatorView.h>
|
||||
#import <HWPanModal/HWDimmedView.h>
|
||||
|
||||
// panModal view
|
||||
#import <HWPanModal/HWPanModalContentView.h>
|
||||
52
Pods/HWPanModal/Sources/KVO/KeyValueObserver.h
generated
Normal file
52
Pods/HWPanModal/Sources/KVO/KeyValueObserver.h
generated
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// KeyValueObserver.h
|
||||
// Lab Color Space Explorer
|
||||
//
|
||||
// Created by Daniel Eggert on 01/12/2013.
|
||||
// Copyright (c) 2013 objc.io. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
|
||||
@interface KeyValueObserver : NSObject
|
||||
|
||||
@property (nonatomic, weak) id target;
|
||||
@property (nonatomic) SEL selector;
|
||||
|
||||
/// Create a Key-Value Observing helper object.
|
||||
///
|
||||
/// As long as the returned token object is retained, the KVO notifications of the @c object
|
||||
/// and @c keyPath will cause the given @c selector to be called on @c target.
|
||||
/// @a object and @a target are weak references.
|
||||
/// Once the token object gets dealloc'ed, the observer gets removed.
|
||||
///
|
||||
/// The @c selector should conform to
|
||||
/// @code
|
||||
/// - (void)nameDidChange:(NSDictionary *)change;
|
||||
/// @endcode
|
||||
/// The passed in dictionary is the KVO change dictionary (c.f. @c NSKeyValueChangeKindKey, @c NSKeyValueChangeNewKey etc.)
|
||||
///
|
||||
/// @returns the opaque token object to be stored in a property
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// @code
|
||||
/// self.nameObserveToken = [KeyValueObserver observeObject:user
|
||||
/// keyPath:@"name"
|
||||
/// target:self
|
||||
/// selector:@selector(nameDidChange:)];
|
||||
/// @endcode
|
||||
+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector __attribute__((warn_unused_result));
|
||||
|
||||
/// Create a key-value-observer with the given KVO options
|
||||
+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector options:(NSKeyValueObservingOptions)options __attribute__((warn_unused_result));
|
||||
|
||||
/**
|
||||
* When you call this method, observer will not work.
|
||||
* Please call observer method again.
|
||||
*/
|
||||
- (void)unObserver;
|
||||
|
||||
@end
|
||||
85
Pods/HWPanModal/Sources/KVO/KeyValueObserver.m
generated
Normal file
85
Pods/HWPanModal/Sources/KVO/KeyValueObserver.m
generated
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// KeyValueObserver.m
|
||||
// Lab Color Space Explorer
|
||||
//
|
||||
// Created by Daniel Eggert on 01/12/2013.
|
||||
// Copyright (c) 2013 objc.io. All rights reserved.
|
||||
//
|
||||
|
||||
#import "KeyValueObserver.h"
|
||||
|
||||
//
|
||||
// Created by chris on 7/24/13.
|
||||
//
|
||||
|
||||
#import "KeyValueObserver.h"
|
||||
|
||||
@interface KeyValueObserver ()
|
||||
@property (nonatomic, weak) id observedObject;
|
||||
@property (nonatomic, copy) NSString* keyPath;
|
||||
@property (nonatomic, assign) BOOL shouldObserver;
|
||||
@end
|
||||
|
||||
@implementation KeyValueObserver
|
||||
|
||||
- (id)initWithObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector options:(NSKeyValueObservingOptions)options;
|
||||
{
|
||||
if (object == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSParameterAssert(target != nil);
|
||||
NSParameterAssert([target respondsToSelector:selector]);
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_shouldObserver = YES;
|
||||
self.target = target;
|
||||
self.selector = selector;
|
||||
self.observedObject = object;
|
||||
self.keyPath = keyPath;
|
||||
[object addObserver:self forKeyPath:keyPath options:options context:(__bridge void *)(self)];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector __attribute__((warn_unused_result));
|
||||
{
|
||||
return [self observeObject:object keyPath:keyPath target:target selector:selector options:0];
|
||||
}
|
||||
|
||||
+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector options:(NSKeyValueObservingOptions)options __attribute__((warn_unused_result));
|
||||
{
|
||||
return [[self alloc] initWithObject:object keyPath:keyPath target:target selector:selector options:options];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
|
||||
{
|
||||
if (context == (__bridge void *)(self)) {
|
||||
[self didChange:change];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didChange:(NSDictionary *)change {
|
||||
|
||||
if (!self.shouldObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
id strongTarget = self.target;
|
||||
|
||||
if ([strongTarget respondsToSelector:self.selector]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[strongTarget performSelector:self.selector withObject:change];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self.observedObject removeObserver:self forKeyPath:self.keyPath];
|
||||
}
|
||||
|
||||
- (void)unObserver {
|
||||
self.shouldObserver = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
102
Pods/HWPanModal/Sources/Mediator/HWPanModalPresentableHandler.h
generated
Normal file
102
Pods/HWPanModal/Sources/Mediator/HWPanModalPresentableHandler.h
generated
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// HWPanModalPresentableHandler.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/10/15.
|
||||
// Copyright © 2019 Heath Wang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
#import <HWPanModal/HWPanModalPanGestureDelegate.h>
|
||||
#import "HWPanModalPresentationDelegate.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, HWPanModalPresentableHandlerMode) {
|
||||
HWPanModalPresentableHandlerModeViewController, // used for UIViewController
|
||||
HWPanModalPresentableHandlerModeView, // used for view
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol HWPanModalPresentableHandlerDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* tell the delegate the presentable is about to update origin y
|
||||
*/
|
||||
- (void)adjustPresentableYPos:(CGFloat)yPos;
|
||||
|
||||
/**
|
||||
* tell the delegate presentable is about to change the form state
|
||||
* @param state short,medium, long
|
||||
*/
|
||||
- (void)presentableTransitionToState:(PresentationState)state;
|
||||
|
||||
|
||||
/**
|
||||
* get current CurrentPresentationState of the delegate
|
||||
*/
|
||||
- (PresentationState)getCurrentPresentationState;
|
||||
|
||||
/**
|
||||
* dismiss Controller/UIView
|
||||
* @param isInteractive only for UIViewController, pop view will ignore it.
|
||||
* @param mode only for UIViewController, pop view will ignore it.
|
||||
*/
|
||||
- (void)dismiss:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode;
|
||||
|
||||
@optional
|
||||
- (void)cancelInteractiveTransition;
|
||||
- (void)finishInteractiveTransition;
|
||||
|
||||
@end
|
||||
|
||||
@protocol HWPanModalPresentableHandlerDataSource <NSObject>
|
||||
|
||||
- (CGSize)containerSize;
|
||||
- (BOOL)isBeingDismissed;
|
||||
- (BOOL)isBeingPresented;
|
||||
- (BOOL)isFormPositionAnimating;
|
||||
|
||||
@optional
|
||||
- (BOOL)isPresentedViewAnchored;
|
||||
- (BOOL)isPresentedControllerInteractive;
|
||||
|
||||
@end
|
||||
|
||||
@interface HWPanModalPresentableHandler : NSObject <UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic, assign, readonly) CGFloat shortFormYPosition;
|
||||
@property (nonatomic, assign, readonly) CGFloat mediumFormYPosition;
|
||||
@property (nonatomic, assign, readonly) CGFloat longFormYPosition;
|
||||
@property (nonatomic, assign, readonly) BOOL extendsPanScrolling;
|
||||
@property (nonatomic, assign, readonly) BOOL anchorModalToLongForm;
|
||||
@property (nonatomic, assign, readonly) CGFloat anchoredYPosition;
|
||||
|
||||
@property (nonatomic, strong, readonly) UIPanGestureRecognizer *panGestureRecognizer;
|
||||
// make controller or view to deal with the gesture action
|
||||
@property (nonatomic, strong, readonly) UIPanGestureRecognizer *screenEdgeGestureRecognizer;
|
||||
|
||||
@property (nonatomic, assign) HWPanModalPresentableHandlerMode mode;
|
||||
@property (nonatomic, weak) UIView<HWPanModalIndicatorProtocol> *dragIndicatorView;
|
||||
@property (nonatomic, weak) UIView *presentedView; // which used to present.
|
||||
|
||||
@property(nonatomic, weak) id <HWPanModalPresentableHandlerDelegate> delegate;
|
||||
@property(nonatomic, weak) id <HWPanModalPresentableHandlerDataSource> dataSource;
|
||||
|
||||
- (instancetype)initWithPresentable:(id <HWPanModalPresentable>)presentable;
|
||||
+ (instancetype)handlerWithPresentable:(id <HWPanModalPresentable>)presentable;
|
||||
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (void)observeScrollable;
|
||||
|
||||
- (void)configureScrollViewInsets;
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated;
|
||||
|
||||
- (void)configureViewLayout;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
769
Pods/HWPanModal/Sources/Mediator/HWPanModalPresentableHandler.m
generated
Normal file
769
Pods/HWPanModal/Sources/Mediator/HWPanModalPresentableHandler.m
generated
Normal file
@@ -0,0 +1,769 @@
|
||||
//
|
||||
// HWPanModalPresentableHandler.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/10/15.
|
||||
// Copyright © 2019 Heath Wang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HWPanModalPresentableHandler.h"
|
||||
#import "UIScrollView+Helper.h"
|
||||
#import "UIViewController+LayoutHelper.h"
|
||||
#import "UIView+HW_Frame.h"
|
||||
#import "KeyValueObserver.h"
|
||||
#import "HWPanModalContentView.h"
|
||||
|
||||
static NSString *const kScrollViewKVOContentOffsetKey = @"contentOffset";
|
||||
|
||||
@interface HWPanModalPresentableHandler ()
|
||||
|
||||
@property (nonatomic, assign) CGFloat shortFormYPosition;
|
||||
|
||||
@property (nonatomic, assign) CGFloat mediumFormYPosition;
|
||||
|
||||
@property (nonatomic, assign) CGFloat longFormYPosition;
|
||||
|
||||
@property (nonatomic, assign) BOOL extendsPanScrolling;
|
||||
|
||||
@property (nonatomic, assign) BOOL anchorModalToLongForm;
|
||||
|
||||
@property (nonatomic, assign) CGFloat anchoredYPosition;
|
||||
|
||||
@property (nonatomic, strong) id<HWPanModalPresentable, HWPanModalPanGestureDelegate> presentable;
|
||||
|
||||
// keyboard handle
|
||||
@property (nonatomic, copy) NSDictionary *keyboardInfo;
|
||||
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *panGestureRecognizer;
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *screenEdgeGestureRecognizer;
|
||||
|
||||
// kvo
|
||||
@property (nonatomic, strong) id observerToken;
|
||||
@property (nonatomic, assign) CGFloat scrollViewYOffset;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalPresentableHandler
|
||||
|
||||
- (instancetype)initWithPresentable:(id <HWPanModalPresentable>)presentable {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_presentable = presentable;
|
||||
_extendsPanScrolling = YES;
|
||||
_anchorModalToLongForm = YES;
|
||||
[self addKeyboardObserver];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)handlerWithPresentable:(id <HWPanModalPresentable>)presentable {
|
||||
return [[self alloc] initWithPresentable:presentable];
|
||||
}
|
||||
|
||||
#pragma mark - Pan Gesture Event Handler
|
||||
|
||||
- (void)didPanOnView:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
if ([self shouldResponseToPanGestureRecognizer:panGestureRecognizer] && !self.keyboardInfo) {
|
||||
|
||||
switch (panGestureRecognizer.state) {
|
||||
|
||||
case UIGestureRecognizerStateBegan:
|
||||
case UIGestureRecognizerStateChanged: {
|
||||
[self handlePanGestureBeginOrChanged:panGestureRecognizer];
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed: {
|
||||
[self handlePanGestureEnded:panGestureRecognizer];
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
} else {
|
||||
[self handlePanGestureDidNotResponse:panGestureRecognizer];
|
||||
}
|
||||
[self.presentable didRespondToPanModalGestureRecognizer:panGestureRecognizer];
|
||||
}
|
||||
|
||||
- (BOOL)shouldResponseToPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
if ([self.presentable shouldRespondToPanModalGestureRecognizer:panGestureRecognizer] ||
|
||||
!(panGestureRecognizer.state == UIGestureRecognizerStateBegan || panGestureRecognizer.state == UIGestureRecognizerStateCancelled)) {
|
||||
|
||||
return ![self shouldFailPanGestureRecognizer:panGestureRecognizer];
|
||||
} else {
|
||||
// stop pan gesture working.
|
||||
panGestureRecognizer.enabled = NO;
|
||||
panGestureRecognizer.enabled = YES;
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)shouldFailPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
if ([self shouldPrioritizePanGestureRecognizer:panGestureRecognizer]) {
|
||||
// high priority than scroll view gesture, disable scrollView gesture.
|
||||
[self.presentable panScrollable].panGestureRecognizer.enabled = NO;
|
||||
[self.presentable panScrollable].panGestureRecognizer.enabled = YES;
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([self shouldHandleShortStatePullDownWithRecognizer:panGestureRecognizer]) {
|
||||
// panGestureRecognizer.enabled = NO;
|
||||
// panGestureRecognizer.enabled = YES;
|
||||
return YES;
|
||||
}
|
||||
|
||||
BOOL shouldFail = NO;
|
||||
UIScrollView *scrollView = [self.presentable panScrollable];
|
||||
if (scrollView) {
|
||||
shouldFail = scrollView.contentOffset.y > -MAX(scrollView.contentInset.top, 0);
|
||||
|
||||
// we want scroll the panScrollable, not the presentedView
|
||||
if (self.isPresentedViewAnchored && shouldFail) {
|
||||
CGPoint location = [panGestureRecognizer locationInView:self.presentedView];
|
||||
BOOL flag = CGRectContainsPoint(scrollView.frame, location) || scrollView.isScrolling;
|
||||
if (flag) {
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
}
|
||||
return flag;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (BOOL)shouldHandleShortStatePullDownWithRecognizer:(UIPanGestureRecognizer *)recognizer {
|
||||
if ([self.presentable allowsPullDownWhenShortState]) return NO;
|
||||
|
||||
CGPoint location = [recognizer translationInView:self.presentedView];
|
||||
if ([self.delegate getCurrentPresentationState] == PresentationStateShort && recognizer.state == UIGestureRecognizerStateBegan) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if ((self.presentedView.frame.origin.y >= self.shortFormYPosition || HW_TWO_FLOAT_IS_EQUAL(self.presentedView.frame.origin.y, self.shortFormYPosition)) && location.y > 0) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)shouldPrioritizePanGestureRecognizer:(UIPanGestureRecognizer *)recognizer {
|
||||
return recognizer.state == UIGestureRecognizerStateBegan && [[self presentable] shouldPrioritizePanModalGestureRecognizer:recognizer];
|
||||
}
|
||||
|
||||
- (void)respondToPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
[self.presentable willRespondToPanModalGestureRecognizer:panGestureRecognizer];
|
||||
|
||||
CGFloat yDisplacement = [panGestureRecognizer translationInView:self.presentedView].y;
|
||||
|
||||
if (self.presentedView.frame.origin.y < self.longFormYPosition) {
|
||||
yDisplacement = yDisplacement / 2;
|
||||
}
|
||||
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(adjustPresentableYPos:)]) {
|
||||
[delegate adjustPresentableYPos:self.presentedView.frame.origin.y + yDisplacement];
|
||||
}
|
||||
|
||||
[panGestureRecognizer setTranslation:CGPointZero inView:self.presentedView];
|
||||
}
|
||||
|
||||
- (BOOL)isVelocityWithinSensitivityRange:(CGFloat)velocity {
|
||||
return (fabs(velocity) - [self.presentable minVerticalVelocityToTriggerDismiss]) > 0;
|
||||
}
|
||||
|
||||
- (CGFloat)nearestDistance:(CGFloat)position inDistances:(NSArray *)distances {
|
||||
|
||||
if (distances.count <= 0) {
|
||||
return position;
|
||||
}
|
||||
|
||||
// TODO: need refine this sort code.
|
||||
NSMutableArray *tmpArr = [NSMutableArray arrayWithCapacity:distances.count];
|
||||
NSMutableDictionary *tmpDict = [NSMutableDictionary dictionaryWithCapacity:distances.count];
|
||||
|
||||
[distances enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
||||
NSNumber *number = obj;
|
||||
NSNumber *absValue = @(fabs(number.floatValue - position));
|
||||
[tmpArr addObject:absValue];
|
||||
tmpDict[absValue] = number;
|
||||
|
||||
}];
|
||||
|
||||
[tmpArr sortUsingSelector:@selector(compare:)];
|
||||
|
||||
NSNumber *result = tmpDict[tmpArr.firstObject];
|
||||
return result.floatValue;
|
||||
}
|
||||
|
||||
- (void)screenEdgeInteractiveAction:(UIPanGestureRecognizer *)gestureRecognizer {
|
||||
//
|
||||
}
|
||||
|
||||
#pragma mark - handle did Pan gesture events
|
||||
|
||||
- (void)handlePanGestureDidNotResponse:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
switch (panGestureRecognizer.state) {
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed: {
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
[self cancelInteractiveTransition];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
[panGestureRecognizer setTranslation:CGPointZero inView:panGestureRecognizer.view];
|
||||
}
|
||||
|
||||
- (void)handlePanGestureBeginOrChanged:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
CGPoint velocity = [panGestureRecognizer velocityInView:self.presentedView];
|
||||
[self respondToPanGestureRecognizer:panGestureRecognizer];
|
||||
|
||||
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
|
||||
// check if toggle dismiss action
|
||||
if ([[self presentable] presentingVCAnimationStyle] > PresentingViewControllerAnimationStyleNone &&
|
||||
velocity.y > 0 &&
|
||||
(self.presentedView.frame.origin.y > self.shortFormYPosition || HW_TWO_FLOAT_IS_EQUAL(self.presentedView.frame.origin.y, self.shortFormYPosition))) {
|
||||
[self dismissPresentable:YES mode:PanModalInteractiveModeDragDown];
|
||||
}
|
||||
}
|
||||
|
||||
if (HW_TWO_FLOAT_IS_EQUAL(self.presentedView.frame.origin.y, self.anchoredYPosition) && self.extendsPanScrolling) {
|
||||
[self.presentable willTransitionToState:PresentationStateLong];
|
||||
}
|
||||
|
||||
// update drag indicator
|
||||
if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
|
||||
if (velocity.y > 0) {
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStatePullDown];
|
||||
} else if (velocity.y < 0 && self.presentedView.frame.origin.y <= self.anchoredYPosition && !self.extendsPanScrolling) {
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handlePanGestureEnded:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
CGPoint velocity = [panGestureRecognizer velocityInView:self.presentedView];
|
||||
/**
|
||||
* pan recognizer结束
|
||||
* 根据velocity(速度),当velocity.y < 0,说明用户在向上拖拽view;当velocity.y > 0,向下拖拽
|
||||
* 根据拖拽的速度,处理不同的情况:
|
||||
* 1.超过拖拽速度阈值时并且向下拖拽,dismiss controller
|
||||
* 2.向上拖拽永远不会dismiss,回弹至相应的状态
|
||||
*/
|
||||
|
||||
if ([self isVelocityWithinSensitivityRange:velocity.y]) {
|
||||
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
PresentationState currentState = [delegate getCurrentPresentationState];
|
||||
|
||||
if (velocity.y < 0) {
|
||||
[self handleDragUpState:currentState];
|
||||
} else {
|
||||
[self handleDragDownState:currentState];
|
||||
}
|
||||
} else {
|
||||
CGFloat position = [self nearestDistance:CGRectGetMinY(self.presentedView.frame) inDistances:@[@([self containerSize].height), @(self.shortFormYPosition), @(self.longFormYPosition), @(self.mediumFormYPosition)]];
|
||||
if (HW_TWO_FLOAT_IS_EQUAL(position, self.longFormYPosition)) {
|
||||
[self transitionToState:PresentationStateLong];
|
||||
[self cancelInteractiveTransition];
|
||||
} else if (HW_TWO_FLOAT_IS_EQUAL(position, self.mediumFormYPosition)) {
|
||||
[self transitionToState:PresentationStateMedium];
|
||||
[self cancelInteractiveTransition];
|
||||
} else if (HW_TWO_FLOAT_IS_EQUAL(position, self.shortFormYPosition) || ![self.presentable allowsDragToDismiss]) {
|
||||
[self transitionToState:PresentationStateShort];
|
||||
[self cancelInteractiveTransition];
|
||||
} else {
|
||||
if ([self isBeingDismissed]) {
|
||||
[self finishInteractiveTransition];
|
||||
} else {
|
||||
[self dismissPresentable:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
}
|
||||
}
|
||||
[self.presentable didEndRespondToPanModalGestureRecognizer:panGestureRecognizer];
|
||||
}
|
||||
|
||||
- (void)handleDragUpState:(PresentationState)state {
|
||||
switch (state) {
|
||||
case PresentationStateLong:
|
||||
[self transitionToState:PresentationStateLong];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
case PresentationStateMedium:
|
||||
[self transitionToState:PresentationStateLong];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
case PresentationStateShort:
|
||||
[self transitionToState:PresentationStateMedium];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleDragDownState:(PresentationState)state {
|
||||
switch (state) {
|
||||
case PresentationStateLong:
|
||||
[self transitionToState:PresentationStateMedium];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
case PresentationStateMedium:
|
||||
[self transitionToState:PresentationStateShort];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
case PresentationStateShort:
|
||||
if (![self.presentable allowsDragToDismiss]) {
|
||||
[self transitionToState:PresentationStateShort];
|
||||
[self cancelInteractiveTransition];
|
||||
} else {
|
||||
if ([self isBeingDismissed]) {
|
||||
[self finishInteractiveTransition];
|
||||
} else {
|
||||
[self dismissPresentable:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollView kvo
|
||||
|
||||
- (void)observeScrollable {
|
||||
UIScrollView *scrollView = [[self presentable] panScrollable];
|
||||
if (!scrollView) {
|
||||
// force set observerToken to nil, make sure to callback.
|
||||
self.observerToken = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
self.scrollViewYOffset = MAX(scrollView.contentOffset.y, -(MAX(scrollView.contentInset.top, 0)));
|
||||
self.observerToken = [KeyValueObserver observeObject:scrollView keyPath:kScrollViewKVOContentOffsetKey target:self selector:@selector(didPanOnScrollViewChanged:) options:NSKeyValueObservingOptionOld];
|
||||
}
|
||||
|
||||
/**
|
||||
As the user scrolls, track & save the scroll view y offset.
|
||||
This helps halt scrolling when we want to hold the scroll view in place.
|
||||
*/
|
||||
- (void)trackScrolling:(UIScrollView *)scrollView {
|
||||
self.scrollViewYOffset = MAX(scrollView.contentOffset.y, -(MAX(scrollView.contentInset.top, 0)));
|
||||
scrollView.showsVerticalScrollIndicator = [[self presentable] showsScrollableVerticalScrollIndicator];
|
||||
}
|
||||
|
||||
/**
|
||||
* Halts the scroll of a given scroll view & anchors it at the `scrollViewYOffset`
|
||||
*/
|
||||
- (void)haltScrolling:(UIScrollView *)scrollView {
|
||||
|
||||
//
|
||||
// Fix bug: the app will crash after the table view reloads data via calling [tableView reloadData] if the user scrolls to the bottom.
|
||||
//
|
||||
// We remove some element and reload data, for example, [self.dataSource removeLastObject], the previous saved scrollViewYOffset value
|
||||
// will be great than or equal to the current actual offset(i.e. scrollView.contentOffset.y). At this time, if the method
|
||||
// [scrollView setContentOffset:CGPointMake(0, self.scrollViewYOffset) animated:NO] is called, which will trigger KVO recursively.
|
||||
// So scrollViewYOffset must be less than or equal to the actual offset here.
|
||||
// See issues: https://github.com/HeathWang/HWPanModal/issues/107 and https://github.com/HeathWang/HWPanModal/issues/103
|
||||
|
||||
if (scrollView.contentOffset.y <= 0 || self.scrollViewYOffset <= scrollView.contentOffset.y) {
|
||||
[scrollView setContentOffset:CGPointMake(0, self.scrollViewYOffset) animated:NO];
|
||||
scrollView.showsVerticalScrollIndicator = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didPanOnScrollViewChanged:(NSDictionary<NSKeyValueChangeKey, id> *)change {
|
||||
|
||||
UIScrollView *scrollView = [[self presentable] panScrollable];
|
||||
if (!scrollView) return;
|
||||
|
||||
if ((![self isBeingDismissed] && ![self isBeingPresented]) ||
|
||||
([self isBeingDismissed] && [self isPresentedViewControllerInteractive])) {
|
||||
|
||||
if (![self isPresentedViewAnchored] && scrollView.contentOffset.y > 0) {
|
||||
[self haltScrolling:scrollView];
|
||||
} else if ([scrollView isScrolling] || [self isPresentedViewAnimating]) {
|
||||
|
||||
// While we're scrolling upwards on the scrollView, store the last content offset position
|
||||
if ([self isPresentedViewAnchored]) {
|
||||
[self trackScrolling:scrollView];
|
||||
} else {
|
||||
/**
|
||||
* Keep scroll view in place while we're panning on main view
|
||||
*/
|
||||
[self haltScrolling:scrollView];
|
||||
}
|
||||
} else {
|
||||
[self trackScrolling:scrollView];
|
||||
}
|
||||
|
||||
} else {
|
||||
/**
|
||||
* 当present Controller,而且动画没有结束的时候,用户可能会对scrollView设置contentOffset
|
||||
* 首次用户滑动scrollView时,会因为scrollViewYOffset = 0而出现错位
|
||||
*/
|
||||
if ([self isBeingPresented]) {
|
||||
[self setScrollableContentOffset:scrollView.contentOffset animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollView update
|
||||
|
||||
- (void)configureScrollViewInsets {
|
||||
|
||||
// when scrolling, return
|
||||
if ([self.presentable panScrollable] && ![self.presentable panScrollable].isScrolling) {
|
||||
UIScrollView *scrollView = [self.presentable panScrollable];
|
||||
// 禁用scrollView indicator除非用户开始滑动scrollView
|
||||
scrollView.showsVerticalScrollIndicator = [self.presentable showsScrollableVerticalScrollIndicator];
|
||||
scrollView.scrollEnabled = [self.presentable isPanScrollEnabled];
|
||||
scrollView.scrollIndicatorInsets = [self.presentable scrollIndicatorInsets];
|
||||
|
||||
if (![self.presentable shouldAutoSetPanScrollContentInset]) return;
|
||||
|
||||
UIEdgeInsets insets1 = scrollView.contentInset;
|
||||
CGFloat bottomLayoutOffset = [UIApplication sharedApplication].keyWindow.rootViewController.bottomLayoutGuide.length;
|
||||
/*
|
||||
* If scrollView has been set contentInset, and bottom is NOT zero, we won't change it.
|
||||
* If contentInset.bottom is zero, set bottom = bottomLayoutOffset
|
||||
* If scrollView has been set contentInset, BUT the bottom < bottomLayoutOffset, set bottom = bottomLayoutOffset
|
||||
*/
|
||||
if (HW_FLOAT_IS_ZERO(insets1.bottom) || insets1.bottom < bottomLayoutOffset) {
|
||||
|
||||
insets1.bottom = bottomLayoutOffset;
|
||||
scrollView.contentInset = insets1;
|
||||
}
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated {
|
||||
if (![self.presentable panScrollable]) return;
|
||||
|
||||
UIScrollView *scrollView = [self.presentable panScrollable];
|
||||
[self.observerToken unObserver];
|
||||
|
||||
[scrollView setContentOffset:offset animated:animated];
|
||||
// wait for animation finished.
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) ((animated ? 0.30 : 0.1) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
[self trackScrolling:scrollView];
|
||||
[self observeScrollable];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - layout
|
||||
|
||||
- (void)configureViewLayout {
|
||||
|
||||
if ([self.presentable isKindOfClass:UIViewController.class]) {
|
||||
UIViewController<HWPanModalPresentable> *layoutPresentable = (UIViewController<HWPanModalPresentable> *) self.presentable;
|
||||
self.shortFormYPosition = layoutPresentable.shortFormYPos;
|
||||
self.mediumFormYPosition = layoutPresentable.mediumFormYPos;
|
||||
self.longFormYPosition = layoutPresentable.longFormYPos;
|
||||
self.anchorModalToLongForm = [layoutPresentable anchorModalToLongForm];
|
||||
self.extendsPanScrolling = [layoutPresentable allowsExtendedPanScrolling];
|
||||
} else if ([self.presentable isKindOfClass:HWPanModalContentView.class]) {
|
||||
HWPanModalContentView<HWPanModalPresentable> *layoutPresentable = (HWPanModalContentView<HWPanModalPresentable> *) self.presentable;
|
||||
self.shortFormYPosition = layoutPresentable.shortFormYPos;
|
||||
self.mediumFormYPosition = layoutPresentable.mediumFormYPos;
|
||||
self.longFormYPosition = layoutPresentable.longFormYPos;
|
||||
self.anchorModalToLongForm = [layoutPresentable anchorModalToLongForm];
|
||||
self.extendsPanScrolling = [layoutPresentable allowsExtendedPanScrolling];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - UIGestureRecognizerDelegate
|
||||
|
||||
/**
|
||||
* ONLY When otherGestureRecognizer is panGestureRecognizer, and target gestureRecognizer is panGestureRecognizer, return YES.
|
||||
*/
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
|
||||
if ([self.presentable respondsToSelector:@selector(hw_gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]) {
|
||||
return [self.presentable hw_gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
|
||||
}
|
||||
|
||||
if ([gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]) {
|
||||
return [otherGestureRecognizer isKindOfClass:UIPanGestureRecognizer.class];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前手势为screenGestureRecognizer时,其他pan recognizer都应该fail
|
||||
*/
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
|
||||
if ([self.presentable respondsToSelector:@selector(hw_gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:)]) {
|
||||
return [self.presentable hw_gestureRecognizer:gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:otherGestureRecognizer];
|
||||
}
|
||||
|
||||
|
||||
if (gestureRecognizer == self.screenEdgeGestureRecognizer && [otherGestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
if ([self.presentable respondsToSelector:@selector(hw_gestureRecognizer:shouldRequireFailureOfGestureRecognizer:)]) {
|
||||
return [self.presentable hw_gestureRecognizer:gestureRecognizer shouldRequireFailureOfGestureRecognizer:otherGestureRecognizer];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
|
||||
|
||||
if ([self.presentable respondsToSelector:@selector(hw_gestureRecognizerShouldBegin:)]) {
|
||||
return [self.presentable hw_gestureRecognizerShouldBegin:gestureRecognizer];
|
||||
}
|
||||
|
||||
if (gestureRecognizer == self.screenEdgeGestureRecognizer) {
|
||||
CGPoint velocity = [self.screenEdgeGestureRecognizer velocityInView:self.screenEdgeGestureRecognizer.view];
|
||||
|
||||
if (velocity.x <= 0 || HW_TWO_FLOAT_IS_EQUAL(velocity.x, 0)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// check the distance to left edge
|
||||
CGPoint location = [self.screenEdgeGestureRecognizer locationInView:self.screenEdgeGestureRecognizer.view];
|
||||
CGFloat thresholdDistance = [[self presentable] maxAllowedDistanceToLeftScreenEdgeForPanInteraction];
|
||||
if (thresholdDistance > 0 && location.x > thresholdDistance) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (velocity.x > 0 && HW_TWO_FLOAT_IS_EQUAL(velocity.y, 0)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
//TODO: this logic can be updated later.
|
||||
if (velocity.x > 0 && velocity.x / fabs(velocity.y) > 2) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - UIKeyboard Handle
|
||||
|
||||
- (void)addKeyboardObserver {
|
||||
if ([self.presentable isAutoHandleKeyboardEnabled]) {
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeKeyboardObserver {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)keyboardWillShow:(NSNotification *)notification {
|
||||
UIView<UIKeyInput> *currentInput = [self findCurrentTextInputInView:self.presentedView];
|
||||
|
||||
if (!currentInput)
|
||||
return;
|
||||
|
||||
self.keyboardInfo = notification.userInfo;
|
||||
[self updatePanContainerFrameForKeyboard];
|
||||
}
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification {
|
||||
self.keyboardInfo = nil;
|
||||
|
||||
NSTimeInterval duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||
UIViewAnimationCurve curve = (UIViewAnimationCurve) [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
|
||||
|
||||
[UIView beginAnimations:nil context:nil];
|
||||
[UIView setAnimationBeginsFromCurrentState:YES];
|
||||
[UIView setAnimationCurve:curve];
|
||||
[UIView setAnimationDuration:duration];
|
||||
|
||||
self.presentedView.transform = CGAffineTransformIdentity;
|
||||
|
||||
[UIView commitAnimations];
|
||||
}
|
||||
|
||||
- (void)updatePanContainerFrameForKeyboard {
|
||||
if (!self.keyboardInfo)
|
||||
return;
|
||||
|
||||
UIView<UIKeyInput> *textInput = [self findCurrentTextInputInView:self.presentedView];
|
||||
if (!textInput)
|
||||
return;
|
||||
|
||||
CGAffineTransform lastTransform = self.presentedView.transform;
|
||||
self.presentedView.transform = CGAffineTransformIdentity;
|
||||
|
||||
CGFloat textViewBottomY = [textInput convertRect:textInput.bounds toView:self.presentedView].origin.y + textInput.hw_height;
|
||||
CGFloat keyboardHeight = [self.keyboardInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
|
||||
|
||||
CGFloat offsetY = 0;
|
||||
CGFloat top = [self.presentable keyboardOffsetFromInputView];
|
||||
offsetY = self.presentedView.hw_height - (keyboardHeight + top + textViewBottomY + self.presentedView.hw_top);
|
||||
|
||||
NSTimeInterval duration = [self.keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||
UIViewAnimationCurve curve = (UIViewAnimationCurve) [self.keyboardInfo[UIKeyboardAnimationCurveUserInfoKey] intValue];
|
||||
|
||||
self.presentedView.transform = lastTransform;
|
||||
[UIView beginAnimations:nil context:NULL];
|
||||
[UIView setAnimationBeginsFromCurrentState:YES];
|
||||
[UIView setAnimationCurve:curve];
|
||||
[UIView setAnimationDuration:duration];
|
||||
|
||||
self.presentedView.transform = CGAffineTransformMakeTranslation(0, offsetY);
|
||||
|
||||
[UIView commitAnimations];
|
||||
}
|
||||
|
||||
- (UIView <UIKeyInput> *)findCurrentTextInputInView:(UIView *)view {
|
||||
if ([view conformsToProtocol:@protocol(UIKeyInput)] && view.isFirstResponder) {
|
||||
// Quick fix for web view issue
|
||||
if ([view isKindOfClass:NSClassFromString(@"UIWebBrowserView")] || [view isKindOfClass:NSClassFromString(@"WKContentView")]) {
|
||||
return nil;
|
||||
}
|
||||
return (UIView <UIKeyInput> *) view;
|
||||
}
|
||||
|
||||
for (UIView *subview in view.subviews) {
|
||||
UIView <UIKeyInput> *inputInView = [self findCurrentTextInputInView:subview];
|
||||
if (inputInView) {
|
||||
return inputInView;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - delegate throw
|
||||
|
||||
- (void)transitionToState:(PresentationState)state {
|
||||
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(presentableTransitionToState:)]) {
|
||||
[delegate presentableTransitionToState:state];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelInteractiveTransition {
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(cancelInteractiveTransition)]) {
|
||||
[delegate cancelInteractiveTransition];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finishInteractiveTransition {
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(finishInteractiveTransition)]) {
|
||||
[delegate finishInteractiveTransition];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dismissPresentable:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode {
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(dismiss:mode:)]) {
|
||||
[delegate dismiss:isInteractive mode:mode];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - dataSource handle
|
||||
|
||||
- (BOOL)isPresentedViewAnchored {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isPresentedViewAnchored)]) {
|
||||
return [self.dataSource isPresentedViewAnchored];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingDismissed {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isBeingDismissed)]) {
|
||||
return [self.dataSource isBeingDismissed];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingPresented {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isBeingPresented)]) {
|
||||
return [self.dataSource isBeingPresented];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isPresentedViewControllerInteractive {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isPresentedControllerInteractive)]) {
|
||||
return [self.dataSource isPresentedControllerInteractive];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isPresentedViewAnimating {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isFormPositionAnimating)]) {
|
||||
[self.dataSource isFormPositionAnimating];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (CGSize)containerSize {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(containerSize)]) {
|
||||
return [self.dataSource containerSize];
|
||||
}
|
||||
|
||||
return CGSizeZero;
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
if (!_panGestureRecognizer) {
|
||||
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didPanOnView:)];
|
||||
_panGestureRecognizer.minimumNumberOfTouches = 1;
|
||||
_panGestureRecognizer.maximumNumberOfTouches = 1;
|
||||
_panGestureRecognizer.delegate = self;
|
||||
}
|
||||
return _panGestureRecognizer;
|
||||
}
|
||||
|
||||
- (UIPanGestureRecognizer *)screenEdgeGestureRecognizer {
|
||||
if (!_screenEdgeGestureRecognizer) {
|
||||
_screenEdgeGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(screenEdgeInteractiveAction:)];
|
||||
_screenEdgeGestureRecognizer.minimumNumberOfTouches = 1;
|
||||
_screenEdgeGestureRecognizer.maximumNumberOfTouches = 1;
|
||||
_screenEdgeGestureRecognizer.delegate = self;
|
||||
}
|
||||
|
||||
return _screenEdgeGestureRecognizer;
|
||||
}
|
||||
|
||||
- (CGFloat)anchoredYPosition {
|
||||
CGFloat defaultTopOffset = [self.presentable topOffset];
|
||||
return self.anchorModalToLongForm ? self.longFormYPosition : defaultTopOffset;
|
||||
}
|
||||
|
||||
#pragma mark - Dealloc
|
||||
|
||||
- (void)dealloc {
|
||||
[self removeKeyboardObserver];
|
||||
}
|
||||
|
||||
@end
|
||||
48
Pods/HWPanModal/Sources/Presentable/HWPanModalHeight.h
generated
Normal file
48
Pods/HWPanModal/Sources/Presentable/HWPanModalHeight.h
generated
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// HWPanModalHeight.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, PanModalHeightType) {
|
||||
PanModalHeightTypeMax NS_SWIFT_NAME(max), // from top max
|
||||
PanModalHeightTypeMaxTopInset NS_SWIFT_NAME(topInset), // from top offset
|
||||
PanModalHeightTypeContent NS_SWIFT_NAME(content), // from bottom
|
||||
PanModalHeightTypeContentIgnoringSafeArea NS_SWIFT_NAME(contentIgnoringSafeArea), // from bottom ignore safeArea
|
||||
PanModalHeightTypeIntrinsic NS_SWIFT_NAME(intrinsic), // auto get size, There is something wrong, DO NOT recommend to use.
|
||||
};
|
||||
|
||||
struct PanModalHeight {
|
||||
PanModalHeightType heightType NS_SWIFT_NAME(type);
|
||||
CGFloat height;
|
||||
};
|
||||
|
||||
typedef struct PanModalHeight PanModalHeight;
|
||||
|
||||
/**
|
||||
* When heightType is PanModalHeightTypeMax, PanModalHeightTypeIntrinsic, the height value will be ignored.
|
||||
*/
|
||||
CG_INLINE PanModalHeight PanModalHeightMake(PanModalHeightType heightType, CGFloat height) {
|
||||
PanModalHeight modalHeight;
|
||||
modalHeight.heightType = heightType;
|
||||
modalHeight.height = height;
|
||||
return modalHeight;
|
||||
}
|
||||
|
||||
static inline BOOL HW_FLOAT_IS_ZERO(CGFloat value) {
|
||||
return (value > -FLT_EPSILON) && (value < FLT_EPSILON);
|
||||
}
|
||||
|
||||
static inline BOOL HW_TWO_FLOAT_IS_EQUAL(CGFloat x, CGFloat y) {
|
||||
CGFloat minusValue = fabs(x - y);
|
||||
CGFloat criticalValue = 0.0001;
|
||||
if (minusValue < criticalValue || minusValue < FLT_MIN) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
31
Pods/HWPanModal/Sources/Presentable/HWPanModalPanGestureDelegate.h
generated
Normal file
31
Pods/HWPanModal/Sources/Presentable/HWPanModalPanGestureDelegate.h
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// HWPanModalPanGestureDelegate.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2022/8/1.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* In this framewok, we use UIPanGestureRecognizer to control user drags behavior.
|
||||
* The internal logic, there are two panGestureRecognizers delegate will response below delegate: the main panGesture used to control darg down, another panGesture used to control screen edge dismiss.
|
||||
* Implement this delegate and custom user drag behavior.
|
||||
* WARNING: BE CAREFUL, AND KNOW WHAT YOU ARE DOING!
|
||||
*/
|
||||
@protocol HWPanModalPanGestureDelegate <NSObject>
|
||||
|
||||
- (BOOL)hw_gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
|
||||
- (BOOL)hw_gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
|
||||
|
||||
- (BOOL)hw_gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
|
||||
- (BOOL)hw_gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
451
Pods/HWPanModal/Sources/Presentable/HWPanModalPresentable.h
generated
Normal file
451
Pods/HWPanModal/Sources/Presentable/HWPanModalPresentable.h
generated
Normal file
@@ -0,0 +1,451 @@
|
||||
//
|
||||
// HWPanModalPresentable.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HWPanModal/HWPanModalHeight.h>
|
||||
#import <HWPanModal/HWPresentingVCAnimatedTransitioning.h>
|
||||
#import <HWPanModal/HWPanModalIndicatorProtocol.h>
|
||||
#import <HWPanModal/HWBackgroundConfig.h>
|
||||
#import <HWPanModal/HWPanModalShadow.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, PresentationState) {
|
||||
PresentationStateShort NS_SWIFT_NAME(short),
|
||||
PresentationStateMedium NS_SWIFT_NAME(medium),
|
||||
PresentationStateLong NS_SWIFT_NAME(long),
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, PresentingViewControllerAnimationStyle) {
|
||||
// no animation for presentingVC
|
||||
PresentingViewControllerAnimationStyleNone NS_SWIFT_NAME(none),
|
||||
// page sheet animation, like iOS13 default modalPresentation style
|
||||
PresentingViewControllerAnimationStylePageSheet NS_SWIFT_NAME(pageSheet),
|
||||
// shopping cart animation, like jd/taobao shopping cart animation
|
||||
PresentingViewControllerAnimationStyleShoppingCart NS_SWIFT_NAME(shoppingCart),
|
||||
// make your own custom animation
|
||||
PresentingViewControllerAnimationStyleCustom NS_SWIFT_NAME(custom),
|
||||
};
|
||||
|
||||
/**
|
||||
* HWPanModalPresentable为present配置协议
|
||||
* 默认情况下无需实现,只需Controller/View适配该协议
|
||||
* 通过category来默认实现以下所有方法,避免继承类
|
||||
*
|
||||
* This Protocol is the core of HWPanModal, we use it to config presentation.
|
||||
* Default, you don't need to conform all of these methods, just implement what you want to customize.
|
||||
* All the config has default value, we use a `UIViewController` category to conform `HWPanModalPresentable` protocol.
|
||||
*/
|
||||
@protocol HWPanModalPresentable <NSObject>
|
||||
|
||||
#pragma mark - ScrollView Config
|
||||
|
||||
/**
|
||||
* 支持同步拖拽的scrollView
|
||||
* 如果ViewController中包含scrollView并且你想scrollView滑动和拖拽手势同时存在,请返回此scrollView
|
||||
*
|
||||
* If your ViewController has a scrollable view(UIScrollView and subclass), and you want pan gesture and scrollable both work, return it.
|
||||
*/
|
||||
- (nullable UIScrollView *)panScrollable;
|
||||
|
||||
/**
|
||||
* determine ScrollView scrollEnabled
|
||||
* default is YES
|
||||
*/
|
||||
- (BOOL)isPanScrollEnabled;
|
||||
|
||||
/**
|
||||
* scrollView指示器insets
|
||||
* Use `panModalSetNeedsLayoutUpdate()` when updating insets.
|
||||
*/
|
||||
- (UIEdgeInsets)scrollIndicatorInsets;
|
||||
|
||||
/**
|
||||
* A Boolean value that controls whether the scrollable vertical scroll indicator is visible.
|
||||
* default is YES.
|
||||
*/
|
||||
- (BOOL)showsScrollableVerticalScrollIndicator;
|
||||
|
||||
/**
|
||||
* default is YES.
|
||||
*/
|
||||
- (BOOL)shouldAutoSetPanScrollContentInset;
|
||||
|
||||
/**
|
||||
* 是否允许拖动额外拖动,如果panScrollable存在,且scrollView contentSize > (size + bottomLayoutOffset),返回YES
|
||||
* 其余情况返回NO
|
||||
*
|
||||
* If panScrollable exists, and scrollView contentSize > (size + bottomLayoutOffset), auto return YES, otherwise return NO.
|
||||
* You can make your own logic if you want, and you know what you are doing.
|
||||
*/
|
||||
- (BOOL)allowsExtendedPanScrolling;
|
||||
|
||||
#pragma mark - Offset/position
|
||||
|
||||
/**
|
||||
* Screen top offset from presented viewController
|
||||
* Default is topLayoutGuide.length + 21.0.
|
||||
*/
|
||||
- (CGFloat)topOffset;
|
||||
|
||||
/**
|
||||
* 当pan状态为short时候的高度
|
||||
* default: shortFormHeight = longFormHeight
|
||||
*/
|
||||
- (PanModalHeight)shortFormHeight;
|
||||
|
||||
/**
|
||||
* default: mediumFormHeight = longFormHeight
|
||||
*/
|
||||
- (PanModalHeight)mediumFormHeight;
|
||||
|
||||
/**
|
||||
* 当pan状态为long的高度
|
||||
*/
|
||||
- (PanModalHeight)longFormHeight;
|
||||
|
||||
/**
|
||||
* 初始弹出高度状态,默认为`shortFormHeight`
|
||||
*
|
||||
* Origin presentation height state, if you have special requirement, change it.
|
||||
* Default is `shortFormHeight`
|
||||
*/
|
||||
- (PresentationState)originPresentationState;
|
||||
|
||||
#pragma mark - Animation config
|
||||
|
||||
/**
|
||||
* spring弹性动画数值
|
||||
* Default is 0.9
|
||||
*/
|
||||
- (CGFloat)springDamping;
|
||||
|
||||
/**
|
||||
* 转场动画时间
|
||||
* Default is 0.5 second
|
||||
*/
|
||||
- (NSTimeInterval)transitionDuration;
|
||||
|
||||
/**
|
||||
* starting from version 0.6.5, Only works when dismiss
|
||||
* Default is same as `- (NSTimeInterval)transitionDuration;`
|
||||
*/
|
||||
- (NSTimeInterval)dismissalDuration;
|
||||
|
||||
/**
|
||||
* 转场动画options
|
||||
* Default is UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState
|
||||
*/
|
||||
- (UIViewAnimationOptions)transitionAnimationOptions;
|
||||
|
||||
#pragma mark - AppearanceTransition
|
||||
|
||||
/**
|
||||
* If enabled, the presenting VC will invoke viewWillAppear:, viewWillDisappear:
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)shouldEnableAppearanceTransition;
|
||||
|
||||
#pragma mark - Background config
|
||||
|
||||
/**
|
||||
* use this object to config background alpha or blur effect
|
||||
* @return background config object
|
||||
*/
|
||||
- (HWBackgroundConfig *)backgroundConfig;
|
||||
|
||||
#pragma mark - User Interaction
|
||||
|
||||
/**
|
||||
* 该bool值控制当pan View状态为long的情况下,是否可以继续拖拽到PanModalHeight = MAX的情况
|
||||
* 默认为YES,即当已经拖拽到long的情况下不能再继续拖动
|
||||
*/
|
||||
- (BOOL)anchorModalToLongForm;
|
||||
|
||||
/**
|
||||
* 是否允许点击背景处dismiss presented Controller
|
||||
* 默认为YES
|
||||
*/
|
||||
- (BOOL)allowsTapBackgroundToDismiss;
|
||||
|
||||
|
||||
/**
|
||||
* 是否允许drag操作dismiss presented Controller
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)allowsDragToDismiss;
|
||||
|
||||
/// Default is YES, When return NO, and you did set shortForm, user CAN NOT pull down the view.
|
||||
- (BOOL)allowsPullDownWhenShortState;
|
||||
|
||||
/**
|
||||
min Velocity from Vertical direction that trigger dismiss action.
|
||||
Default is 300.0
|
||||
*/
|
||||
- (CGFloat)minVerticalVelocityToTriggerDismiss;
|
||||
|
||||
/**
|
||||
* 是否允许用户操作
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)isUserInteractionEnabled;
|
||||
|
||||
/**
|
||||
* 是否允许触觉反馈
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)isHapticFeedbackEnabled;
|
||||
|
||||
/**
|
||||
* 是否允许触摸事件透传到presenting ViewController/View。如果你有特殊需求的话(比如弹出一个底部视图,但是你想操作弹出视图下面的view,即presenting VC/View),可开启此功能
|
||||
*
|
||||
* Whether allows touch events passing through the transition container view.
|
||||
* In some situations, you present the bottom VC/View, and you want to operate the presenting VC/View(mapView, scrollView and etc), enable this func.
|
||||
*
|
||||
* Note: You SHOULD MUST dismiss the presented VC in the right time.
|
||||
*/
|
||||
- (BOOL)allowsTouchEventsPassingThroughTransitionView;
|
||||
|
||||
#pragma mark - Screen left egde interaction
|
||||
|
||||
/**
|
||||
* 是否允许屏幕边缘侧滑手势
|
||||
* Default is NO,not allowed this user interaction.
|
||||
*
|
||||
* Note: Currently only works on UIViewController.
|
||||
*/
|
||||
- (BOOL)allowScreenEdgeInteractive;
|
||||
|
||||
/**
|
||||
* Max allowed distance to screen left edge when you want to make screen edge pan interaction
|
||||
* Default is 0, means it will ignore this limit, full screen left edge pan will work.
|
||||
* @return distance to left screen edge
|
||||
*/
|
||||
- (CGFloat)maxAllowedDistanceToLeftScreenEdgeForPanInteraction;
|
||||
|
||||
/**
|
||||
* When you enabled `- (BOOL)allowScreenEdgeInteractive`, this can work.
|
||||
* min horizontal velocity to trigger screen edge dismiss if the drag didn't reach 0.5 screen width.
|
||||
* Default is 500
|
||||
*/
|
||||
- (CGFloat)minHorizontalVelocityToTriggerScreenEdgeDismiss;
|
||||
|
||||
#pragma mark - Customize presentingViewController animation
|
||||
|
||||
/**
|
||||
* Config presentingViewController animation style, this animations will work for present & dismiss.
|
||||
* Default is `PresentingViewControllerAnimationStyleNone`.
|
||||
* @return The animation style.
|
||||
*/
|
||||
- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle;
|
||||
|
||||
/**
|
||||
* 自定义presenting ViewController转场动画,默认为nil
|
||||
* 注意:如果实现该方法并返回非空示例,要使该方法生效,`- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle`必须返回PresentingViewControllerAnimationStyleCustom
|
||||
*
|
||||
* custom presenting ViewController transition animation, default is nil
|
||||
* Note: If you implement this method and return non nil value, You must implement `- (PresentingViewControllerAnimationStyle)
|
||||
* presentingVCAnimationStyle` and return PresentingViewControllerAnimationStyleCustom
|
||||
*/
|
||||
- (nullable id<HWPresentingViewControllerAnimatedTransitioning>)customPresentingVCAnimation;
|
||||
|
||||
#pragma mark - Content UI config
|
||||
|
||||
/**
|
||||
* 是否顶部圆角
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)shouldRoundTopCorners;
|
||||
|
||||
/**
|
||||
* 顶部圆角数值
|
||||
* Default is 8.0
|
||||
*/
|
||||
- (CGFloat)cornerRadius;
|
||||
|
||||
/**
|
||||
* presented content shadow
|
||||
* Default is None config
|
||||
*/
|
||||
- (HWPanModalShadow *)contentShadow;
|
||||
|
||||
#pragma mark - Indicator config
|
||||
|
||||
/**
|
||||
* 是否显示drag指示view
|
||||
* Default is YES,Default this method depend on `- (BOOL)shouldRoundTopCorners`
|
||||
*/
|
||||
- (BOOL)showDragIndicator;
|
||||
|
||||
/**
|
||||
* You can make the indicator customized. Just adopt `HWPanModalIndicatorProtocol`
|
||||
* Default this method return nil, Then the default indicator will be used.
|
||||
*/
|
||||
- (__kindof UIView<HWPanModalIndicatorProtocol> * _Nullable)customIndicatorView;
|
||||
|
||||
#pragma mark - Keyboard handle
|
||||
|
||||
/**
|
||||
* When there is text input view exists and becomeFirstResponder, will auto handle keyboard height.
|
||||
* Default is YES. You can disable it, handle it by yourself.
|
||||
*/
|
||||
- (BOOL)isAutoHandleKeyboardEnabled;
|
||||
|
||||
/**
|
||||
The offset that keyboard show from input view's bottom. It works when
|
||||
`isAutoHandleKeyboardEnabled` return YES.
|
||||
|
||||
@return offset, default is 5.
|
||||
*/
|
||||
- (CGFloat)keyboardOffsetFromInputView;
|
||||
|
||||
#pragma mark - Delegate
|
||||
|
||||
#pragma mark - Pan Gesture delegate
|
||||
|
||||
/**
|
||||
* 询问delegate是否需要使拖拽手势生效
|
||||
* 若返回NO,则禁用拖拽手势操作,即不能拖拽dismiss
|
||||
* 默认为YES
|
||||
*/
|
||||
- (BOOL)shouldRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* 当pan recognizer状态为begin/changed时,通知delegate回调。
|
||||
* 当拖动presented View时,该方法会持续的回调
|
||||
* 默认实现为空
|
||||
*/
|
||||
- (void)willRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* 内部处理完成拖动操作后触发此回调,此时view frame可能已经变化。
|
||||
* Framework has did finish logic for GestureRecognizer delegate. It will call many times when you darg.
|
||||
*/
|
||||
- (void)didRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* 内部处理完成拖动操作后触发此回调,此时view frame可能已经变化。
|
||||
* Framework has did finish logic for GestureRecognizer delegate. It will call many times when you darg.
|
||||
*/
|
||||
- (void)didEndRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* 是否优先执行dismiss拖拽手势,当存在panScrollable的情况下,如果此方法返回YES,则
|
||||
* dismiss手势生效,scrollView本身的滑动则不再生效。也就是说可以拖动Controller view,而scrollView没法拖动了。
|
||||
*
|
||||
* 例子:controller view上添加一个TableView,并铺满全屏,然后在controller view 顶部添加一个一定大小的viewA,
|
||||
* 这个时候会发现viewA有时候无法拖动,可以实现此delegate方法来解决
|
||||
```
|
||||
- (BOOL)shouldPrioritizePanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
CGPoint loc = [panGestureRecognizer locationInView:self.view];
|
||||
// check whether user pan action in viewA
|
||||
if (CGRectContainsPoint(self.viewA.frame, loc)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
```
|
||||
* 默认为NO
|
||||
*
|
||||
* This delegate is useful when you want panGestureRecognizer has a high prioritize and
|
||||
* make scrollable does NOT scroll.
|
||||
* Example: You controller add a full size tableView, then add viewA on top of your controller view.
|
||||
* Now you find you can not drag the viewA, use this delegate to resolve problem.
|
||||
* Please refer to code above this comment.
|
||||
*
|
||||
* Default is NO
|
||||
*/
|
||||
- (BOOL)shouldPrioritizePanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* When you pan present controller to dismiss, and the view's y <= shortFormYPos,
|
||||
* this delegate method will be called.
|
||||
* @param percent 0 ~ 1, 1 means has dismissed
|
||||
*/
|
||||
- (void)panModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer dismissPercent:(CGFloat)percent;
|
||||
|
||||
#pragma mark - PresentationState change delegate
|
||||
/**
|
||||
* 是否应该变更panModal状态
|
||||
*/
|
||||
- (BOOL)shouldTransitionToState:(PresentationState)state;
|
||||
|
||||
/**
|
||||
* called when the Transition State will change.
|
||||
* 通知回调即将变更状态
|
||||
*/
|
||||
- (void)willTransitionToState:(PresentationState)state;
|
||||
|
||||
/**
|
||||
* PresentationState did change callback
|
||||
*/
|
||||
- (void)didChangeTransitionToState:(PresentationState)state;
|
||||
|
||||
#pragma mark - present delegate
|
||||
|
||||
/**
|
||||
* call when present transition will begin.
|
||||
*/
|
||||
- (void)panModalTransitionWillBegin;
|
||||
|
||||
/**
|
||||
* call when present transition did finish.
|
||||
*/
|
||||
- (void)panModalTransitionDidFinish;
|
||||
|
||||
/**
|
||||
* call when your custom presented vc has been added to the presentation container.
|
||||
*/
|
||||
- (void)presentedViewDidMoveToSuperView;
|
||||
|
||||
#pragma mark - Dismiss delegate
|
||||
/**
|
||||
* will dismiss
|
||||
*/
|
||||
- (void)panModalWillDismiss;
|
||||
|
||||
/**
|
||||
* Did finish dismissing
|
||||
*/
|
||||
- (void)panModalDidDismissed;
|
||||
|
||||
#pragma mark - DEPRECATED DECLARE
|
||||
|
||||
/**
|
||||
* 是否对presentingViewController做动画效果,默认该效果类似淘宝/京东购物车凹陷效果
|
||||
* 默认为NO
|
||||
*/
|
||||
- (BOOL)shouldAnimatePresentingVC DEPRECATED_MSG_ATTRIBUTE("This api has been marked as DEPRECATED on version 0.3.6, please use `- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle` replaced.");
|
||||
|
||||
/**
|
||||
* 背景透明度
|
||||
* Default is 0.7
|
||||
*/
|
||||
- (CGFloat)backgroundAlpha DEPRECATED_MSG_ATTRIBUTE("This api has been marked as DEPRECATED on version 0.7.0, please use `- (HWBackgroundConfig *)backgroundConfig` replaced.");
|
||||
|
||||
/**
|
||||
* Blur background
|
||||
* This function can NOT coexist with backgroundAlpha
|
||||
* Default use backgroundAlpha, Once you set backgroundBlurRadius > 0, blur will work.
|
||||
* Recommend set the value 10 ~ 20.
|
||||
* @return blur radius
|
||||
*/
|
||||
- (CGFloat)backgroundBlurRadius DEPRECATED_MSG_ATTRIBUTE("This api has been marked as DEPRECATED on version 0.7.0, please use `- (HWBackgroundConfig *)backgroundConfig` replaced.");
|
||||
|
||||
/**
|
||||
* blur background color
|
||||
* @return color, default is White Color.
|
||||
*/
|
||||
- (nonnull UIColor *)backgroundBlurColor DEPRECATED_MSG_ATTRIBUTE("This api has been marked as DEPRECATED on version 0.7.0, please use `- (HWBackgroundConfig *)backgroundConfig` replaced.");
|
||||
|
||||
@end
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user