Compare commits
47 Commits
efb04d134e
...
5af2612ff7
| Author | SHA1 | Date | |
|---|---|---|---|
| 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.keyBoardst.shared</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -6,74 +6,215 @@
|
||||
//
|
||||
|
||||
#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"
|
||||
|
||||
@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; // 设置页
|
||||
@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
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
- (void)setupUI {
|
||||
CGFloat toolBarHeight = 40;
|
||||
CGFloat bottom = 5;
|
||||
CGFloat buttonSpace = 8;
|
||||
CGFloat eachButtonHeight = (KEYBOARDHEIGHT - toolBarHeight - 10 - 8 * 3 - bottom) / 4;
|
||||
// 固定键盘整体高度
|
||||
[self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT].active = YES;
|
||||
// 预置功能面板(默认隐藏),与键盘区域共享相同布局
|
||||
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);
|
||||
}];
|
||||
|
||||
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 30)];
|
||||
view.backgroundColor = [UIColor redColor];
|
||||
[self.view addSubview:view];
|
||||
[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);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
- (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.
|
||||
}
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)textDidChange:(id<UITextInput>)textInput {
|
||||
// The app has just changed the document's contents, the document context has been updated.
|
||||
/// 切换显示功能面板/键盘主视图
|
||||
- (void)showFunctionPanel:(BOOL)show {
|
||||
// 简单显隐切换,复用相同的布局区域
|
||||
self.functionView.hidden = !show;
|
||||
self.keyBoardMainView.hidden = show;
|
||||
|
||||
UIColor *textColor = nil;
|
||||
if (self.textDocumentProxy.keyboardAppearance == UIKeyboardAppearanceDark) {
|
||||
textColor = [UIColor whiteColor];
|
||||
// 可选:把当前显示的视图置顶,避免层级遮挡
|
||||
if (show) {
|
||||
[self.view bringSubviewToFront:self.functionView];
|
||||
} else {
|
||||
textColor = [UIColor blackColor];
|
||||
[self.view bringSubviewToFront:self.keyBoardMainView];
|
||||
}
|
||||
[self.nextKeyboardButton setTitleColor:textColor forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
/// 显示/隐藏设置页(高度与 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 {
|
||||
NSURL *url = [NSURL URLWithString:@"kbkeyboard://login?src=keyboard"];
|
||||
if (!url) return;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {
|
||||
// 即使失败也不重复尝试;避免打扰。
|
||||
__unused typeof(weakSelf) selfStrong = weakSelf;
|
||||
}];
|
||||
}
|
||||
@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,20 @@
|
||||
// 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://your.domain/ul" // 替换为你的真实域名与前缀路径
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
|
||||
|
||||
#endif /* PrefixHeader_pch */
|
||||
|
||||
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
|
||||
|
||||
185
CustomKeyboard/View/KBFullAccessGuideView.m
Normal file
185
CustomKeyboard/View/KBFullAccessGuideView.m
Normal file
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// KBFullAccessGuideView.m
|
||||
// CustomKeyboard
|
||||
// 没有开启完全访问的提示框
|
||||
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "Masonry.h"
|
||||
|
||||
@interface KBFullAccessGuideView ()
|
||||
@property (nonatomic, strong) UIControl *backdrop;
|
||||
@property (nonatomic, strong) UIView *card;
|
||||
@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 {
|
||||
UIView *container = parent.window ?: parent;
|
||||
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;
|
||||
// 避免重复
|
||||
for (UIView *v in (parent.window ?: parent).subviews) {
|
||||
if ([v isKindOfClass:[KBFullAccessGuideView class]]) return;
|
||||
}
|
||||
[[KBFullAccessGuideView build] presentIn:parent];
|
||||
}
|
||||
|
||||
+ (void)dismissFromView:(UIView *)parent {
|
||||
UIView *container = parent.window ?: parent;
|
||||
for (UIView *v in container.subviews) {
|
||||
if ([v isKindOfClass:[KBFullAccessGuideView class]]) {
|
||||
[(KBFullAccessGuideView *)v dismiss];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (UIInputViewController *)kb_findInputController {
|
||||
UIResponder *res = self;
|
||||
while (res) {
|
||||
if ([res isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)res;
|
||||
}
|
||||
res = res.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)onTapGoEnable {
|
||||
// 在扩展中无法使用 UIApplication。改为委托宿主打开链接:
|
||||
// 方案:优先拉起主 App 并由主 App 打开设置页,避免宿主拦截。
|
||||
UIInputViewController *ivc = [self kb_findInputController];
|
||||
if (!ivc) { [self dismiss]; return; }
|
||||
|
||||
// 先尝试 Universal Link(如未配置可改为你的域名),失败再用自定义 scheme。
|
||||
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]];
|
||||
void (^fallback)(void) = ^{
|
||||
NSURL *scheme = [NSURL URLWithString:@"kbkeyboard://settings?src=kb_extension"]; // 主App在 openURL 中处理
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(__unused BOOL ok2) {
|
||||
// 无论成功与否,都收起当前提示层,避免遮挡
|
||||
[self dismiss];
|
||||
}];
|
||||
};
|
||||
|
||||
if (ul) {
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
|
||||
if (ok) { [self dismiss]; }
|
||||
else { fallback(); }
|
||||
}];
|
||||
} else {
|
||||
fallback();
|
||||
}
|
||||
}
|
||||
@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
|
||||
161
CustomKeyboard/View/KBFunctionBarView.m
Normal file
161
CustomKeyboard/View/KBFunctionBarView.m
Normal file
@@ -0,0 +1,161 @@
|
||||
//
|
||||
// KBFunctionBarView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
// 功能 - barview
|
||||
|
||||
#import "KBFunctionBarView.h"
|
||||
#import "Masonry.h"
|
||||
|
||||
@interface KBFunctionBarView ()
|
||||
@property (nonatomic, strong) UIView *leftContainer; // 左侧按钮容器
|
||||
@property (nonatomic, strong) UIView *rightContainer; // 右侧按钮容器
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *rightButtonsInternal;
|
||||
@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.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.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
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);
|
||||
}];
|
||||
|
||||
// 左侧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;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
38
CustomKeyboard/View/KBFunctionView.h
Normal file
38
CustomKeyboard/View/KBFunctionView.h
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// 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; // 右侧-发送
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
432
CustomKeyboard/View/KBFunctionView.m
Normal file
432
CustomKeyboard/View/KBFunctionView.m
Normal file
@@ -0,0 +1,432 @@
|
||||
//
|
||||
// KBFunctionView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBFunctionBarView.h"
|
||||
#import "KBFunctionPasteView.h"
|
||||
#import "KBFunctionTagCell.h"
|
||||
#import "Masonry.h"
|
||||
#import <MBProgressHUD.h>
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "KBFullAccessManager.h"
|
||||
|
||||
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.backgroundColor = [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
|
||||
|
||||
[self setupUI];
|
||||
[self reloadDemoData];
|
||||
|
||||
// 初始化剪贴板监控状态
|
||||
_lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
|
||||
#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;
|
||||
CGFloat vSpace = 10;
|
||||
[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);
|
||||
make.height.mas_equalTo(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 = [self findInputViewController];
|
||||
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 成功
|
||||
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"kbkeyboard://login?src=functionView&index=%ld&title=%@", (long)indexPath.item, encodedTitle]];
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
|
||||
if (!ok2) {
|
||||
// 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 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 (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];
|
||||
if (self.window && !self.isHidden) {
|
||||
[self startPasteboardMonitor];
|
||||
} else {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden {
|
||||
BOOL wasHidden = self.isHidden;
|
||||
[super setHidden:hidden];
|
||||
if (wasHidden != hidden) {
|
||||
if (!hidden && self.window) {
|
||||
[self startPasteboardMonitor];
|
||||
} else {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
}
|
||||
}
|
||||
- (void)onTapDelete {
|
||||
NSLog(@"点击:删除");
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
- (void)onTapClear {
|
||||
NSLog(@"点击:清空");
|
||||
// 连续删除:仅清空光标之前的输入(不改动 pasteView 的内容)
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
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 = [self findInputViewController];
|
||||
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
|
||||
|
||||
// 在视图的响应链中查找宿主 UIInputViewController(KeyboardViewController)
|
||||
- (UIInputViewController *)findInputViewController {
|
||||
UIResponder *responder = self;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
33
CustomKeyboard/View/KBKeyBoardMainView.h
Normal file
33
CustomKeyboard/View/KBKeyBoardMainView.h
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
118
CustomKeyboard/View/KBKeyBoardMainView.m
Normal file
118
CustomKeyboard/View/KBKeyBoardMainView.m
Normal file
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// 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"
|
||||
|
||||
@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 = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
// 顶部栏
|
||||
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 统一处理
|
||||
|
||||
@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
|
||||
56
CustomKeyboard/View/KBKeyButton.m
Normal file
56
CustomKeyboard/View/KBKeyButton.m
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// KBKeyButton.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBKeyButton.h"
|
||||
#import "KBKey.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]; // 字体样式
|
||||
[self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
[self setTitleColor:[UIColor blackColor] forState:UIControlStateHighlighted];
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
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 等特殊按键的高亮显示
|
||||
if (self.isSelected) {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0];
|
||||
} else {
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
303
CustomKeyboard/View/KBKeyboardView.m
Normal file
303
CustomKeyboard/View/KBKeyboardView.m
Normal file
@@ -0,0 +1,303 @@
|
||||
//
|
||||
// KBKeyboardView.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
|
||||
#import "KBKeyboardView.h"
|
||||
#import "KBKeyButton.h"
|
||||
#import "KBKey.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;
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
_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];
|
||||
|
||||
// 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;
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
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,144 @@
|
||||
// KBToolBar.m
|
||||
// CustomKeyboard
|
||||
//
|
||||
// Created by Mac on 2025/10/27.
|
||||
// Created by Mac on 2025/10/28.
|
||||
//
|
||||
|
||||
#import "KBToolBar.h"
|
||||
|
||||
@interface KBToolBar ()
|
||||
@property (nonatomic, strong) UIView *leftContainer;
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
||||
@property (nonatomic, strong) UIButton *settingsButtonInternal;
|
||||
@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.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.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
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);
|
||||
}];
|
||||
|
||||
// 在左侧容器中创建 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;
|
||||
}
|
||||
|
||||
- (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;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
26
Podfile
26
Podfile
@@ -2,14 +2,6 @@
|
||||
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!
|
||||
@@ -17,8 +9,24 @@ target 'keyBoard' do
|
||||
pod 'AFNetworking','4.0.1'
|
||||
pod 'Bugly','2.6.1'
|
||||
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 '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
|
||||
|
||||
|
||||
|
||||
17
Podfile.lock
17
Podfile.lock
@@ -15,7 +15,12 @@ PODS:
|
||||
- AFNetworking/UIKit (4.0.1):
|
||||
- AFNetworking/NSURLSession
|
||||
- Bugly (2.6.1)
|
||||
- DZNEmptyDataSet (1.8.1)
|
||||
- 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):
|
||||
@@ -25,16 +30,23 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- AFNetworking (= 4.0.1)
|
||||
- Bugly (= 2.6.1)
|
||||
- DZNEmptyDataSet (= 1.8.1)
|
||||
- LookinServer
|
||||
- Masonry (= 1.1.0)
|
||||
- MBProgressHUD (= 1.2.0)
|
||||
- MJExtension (= 3.4.2)
|
||||
- MJRefresh (= 3.7.9)
|
||||
- SDWebImage
|
||||
- SDWebImage (= 5.21.1)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/CocoaPods/Specs.git:
|
||||
- AFNetworking
|
||||
- Bugly
|
||||
- DZNEmptyDataSet
|
||||
- LookinServer
|
||||
- Masonry
|
||||
- MBProgressHUD
|
||||
- MJExtension
|
||||
- MJRefresh
|
||||
- SDWebImage
|
||||
@@ -42,11 +54,14 @@ SPEC REPOS:
|
||||
SPEC CHECKSUMS:
|
||||
AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58
|
||||
Bugly: 217ac2ce5f0f2626d43dbaa4f70764c953a26a31
|
||||
DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7
|
||||
LookinServer: 1b2b61c6402ae29fa22182d48f5cd067b4e99e80
|
||||
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
|
||||
MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406
|
||||
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
|
||||
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
|
||||
PODFILE CHECKSUM: b3c72fe500149c35040cdd73c1d91fe05777bc5f
|
||||
PODFILE CHECKSUM: 6e0fcb7cf319a770ac7883c7cf3b1df1a6829e77
|
||||
|
||||
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
21
Pods/LookinServer/LICENSE
generated
Normal file
21
Pods/LookinServer/LICENSE
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [2023] [LI KAI]
|
||||
|
||||
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.
|
||||
73
Pods/LookinServer/README.md
generated
Normal file
73
Pods/LookinServer/README.md
generated
Normal file
@@ -0,0 +1,73 @@
|
||||

|
||||
|
||||
# Introduction
|
||||
You can inspect and modify views in iOS app via Lookin, just like UI Inspector in Xcode, or another app called Reveal.
|
||||
|
||||
Official Website:https://lookin.work/
|
||||
|
||||
# Integration Guide
|
||||
To use Lookin macOS app, you need to integrate LookinServer (iOS Framework of Lookin) into your iOS project.
|
||||
|
||||
> **Warning**
|
||||
> 1. Never integrate LookinServer in Release building configuration.
|
||||
> 2. Do not use versions earlier than 1.0.6, as it contains a critical bug that could lead to online incidents in your project: https://qxh1ndiez2w.feishu.cn/wiki/Z9SpwT7zWiqvYvkBe7Lc6Disnab
|
||||
|
||||
## via CocoaPods:
|
||||
### Swift Project
|
||||
`pod 'LookinServer', :subspecs => ['Swift'], :configurations => ['Debug']`
|
||||
### Objective-C Project
|
||||
`pod 'LookinServer', :configurations => ['Debug']`
|
||||
## via Swift Package Manager:
|
||||
`https://github.com/QMUI/LookinServer/`
|
||||
|
||||
# Repository
|
||||
LookinServer: https://github.com/QMUI/LookinServer
|
||||
|
||||
macOS app: https://github.com/hughkli/Lookin/
|
||||
|
||||
# Tips
|
||||
- How to display custom information in Lookin: https://bytedance.larkoffice.com/docx/TRridRXeUoErMTxs94bcnGchnlb
|
||||
- How to display more member variables in Lookin: https://bytedance.larkoffice.com/docx/CKRndHqdeoub11xSqUZcMlFhnWe
|
||||
- How to turn on Swift optimization for Lookin: https://bytedance.larkoffice.com/docx/GFRLdzpeKoakeyxvwgCcZ5XdnTb
|
||||
- Documentation Collection: https://bytedance.larkoffice.com/docx/Yvv1d57XQoe5l0xZ0ZRc0ILfnWb
|
||||
|
||||
# Acknowledgements
|
||||
https://qxh1ndiez2w.feishu.cn/docx/YIFjdE4gIolp3hxn1tGckiBxnWf
|
||||
|
||||
---
|
||||
# 简介
|
||||
Lookin 可以查看与修改 iOS App 里的 UI 对象,类似于 Xcode 自带的 UI Inspector 工具,或另一款叫做 Reveal 的软件。
|
||||
|
||||
官网:https://lookin.work/
|
||||
|
||||
# 安装 LookinServer Framework
|
||||
如果这是你的 iOS 项目第一次使用 Lookin,则需要先把 LookinServer 这款 iOS Framework 集成到你的 iOS 项目中。
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> 1. 不要在 AppStore 模式下集成 LookinServer。
|
||||
> 2. 不要使用早于 1.0.6 的版本,因为它包含一个严重 Bug,可能导致线上事故: https://qxh1ndiez2w.feishu.cn/wiki/Z9SpwT7zWiqvYvkBe7Lc6Disnab
|
||||
## 通过 CocoaPods:
|
||||
|
||||
### Swift 项目
|
||||
`pod 'LookinServer', :subspecs => ['Swift'], :configurations => ['Debug']`
|
||||
### Objective-C 项目
|
||||
`pod 'LookinServer', :configurations => ['Debug']`
|
||||
|
||||
## 通过 Swift Package Manager:
|
||||
`https://github.com/QMUI/LookinServer/`
|
||||
|
||||
# 源代码仓库
|
||||
|
||||
iOS 端 LookinServer:https://github.com/QMUI/LookinServer
|
||||
|
||||
macOS 端软件:https://github.com/hughkli/Lookin/
|
||||
|
||||
# 技巧
|
||||
- 如何在 Lookin 中展示自定义信息: https://bytedance.larkoffice.com/docx/TRridRXeUoErMTxs94bcnGchnlb
|
||||
- 如何在 Lookin 中展示更多成员变量: https://bytedance.larkoffice.com/docx/CKRndHqdeoub11xSqUZcMlFhnWe
|
||||
- 如何为 Lookin 开启 Swift 优化: https://bytedance.larkoffice.com/docx/GFRLdzpeKoakeyxvwgCcZ5XdnTb
|
||||
- 文档汇总:https://bytedance.larkoffice.com/docx/Yvv1d57XQoe5l0xZ0ZRc0ILfnWb
|
||||
|
||||
# 鸣谢
|
||||
https://qxh1ndiez2w.feishu.cn/docx/YIFjdE4gIolp3hxn1tGckiBxnWf
|
||||
40
Pods/LookinServer/Src/Base/LookinIvarTrace.h
generated
Normal file
40
Pods/LookinServer/Src/Base/LookinIvarTrace.h
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// LookinIvarTrace.h
|
||||
// Lookin
|
||||
//
|
||||
// Created by Li Kai on 2019/4/30.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
extern NSString *const LookinIvarTraceRelationValue_Self;
|
||||
|
||||
/// 如果 hostClassName 和 ivarName 均 equal,则认为两个 LookinIvarTrace 对象彼此 equal
|
||||
/// 比如 A 是 B 的 superview,且 A 的 "_stageView" 指向 B,则 B 会有一个 LookinIvarTrace:hostType 为 “superview”,hostClassName 为 A 的 class,ivarName 为 “_stageView”
|
||||
@interface LookinIvarTrace : NSObject <NSSecureCoding, NSCopying>
|
||||
|
||||
/// 该值可能是 "superview"、"superlayer"、“self” 或 nil
|
||||
@property(nonatomic, copy) NSString *relation;
|
||||
|
||||
@property(nonatomic, copy) NSString *hostClassName;
|
||||
|
||||
@property(nonatomic, copy) NSString *ivarName;
|
||||
|
||||
#pragma mark - No Coding
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
@property(nonatomic, weak) id hostObject;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@interface NSObject (LookinServerTrace)
|
||||
|
||||
@property(nonatomic, copy) NSArray<LookinIvarTrace *> *lks_ivarTraces;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
70
Pods/LookinServer/Src/Base/LookinIvarTrace.m
generated
Normal file
70
Pods/LookinServer/Src/Base/LookinIvarTrace.m
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// LookinIvarTrace.m
|
||||
// Lookin
|
||||
//
|
||||
// Created by Li Kai on 2019/4/30.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "LookinIvarTrace.h"
|
||||
|
||||
NSString *const LookinIvarTraceRelationValue_Self = @"self";
|
||||
|
||||
@implementation LookinIvarTrace
|
||||
|
||||
#pragma mark - Equal
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return self.hostClassName.hash ^ self.ivarName.hash;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
if (self == object) {
|
||||
return YES;
|
||||
}
|
||||
if (![object isKindOfClass:[LookinIvarTrace class]]) {
|
||||
return NO;
|
||||
}
|
||||
LookinIvarTrace *comparedObj = object;
|
||||
if ([self.hostClassName isEqualToString:comparedObj.hostClassName] && [self.ivarName isEqualToString:comparedObj.ivarName]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - <NSCopying>
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
LookinIvarTrace *newTrace = [[LookinIvarTrace allocWithZone:zone] init];
|
||||
newTrace.relation = self.relation;
|
||||
newTrace.hostClassName = self.hostClassName;
|
||||
newTrace.ivarName = self.ivarName;
|
||||
return newTrace;
|
||||
}
|
||||
|
||||
#pragma mark - <NSCoding>
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder {
|
||||
[aCoder encodeObject:self.relation forKey:@"relation"];
|
||||
[aCoder encodeObject:self.hostClassName forKey:@"hostClassName"];
|
||||
[aCoder encodeObject:self.ivarName forKey:@"ivarName"];
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||||
if (self = [super init]) {
|
||||
self.relation = [aDecoder decodeObjectForKey:@"relation"];
|
||||
self.hostClassName = [aDecoder decodeObjectForKey:@"hostClassName"];
|
||||
self.ivarName = [aDecoder decodeObjectForKey:@"ivarName"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
41
Pods/LookinServer/Src/Main/Server/Category/CALayer+LookinServer.h
generated
Normal file
41
Pods/LookinServer/Src/Main/Server/Category/CALayer+LookinServer.h
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIView+LookinMobile.h
|
||||
// WeRead
|
||||
//
|
||||
// Created by Li Kai on 2018/11/30.
|
||||
// Copyright © 2018 tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "LookinDefines.h"
|
||||
#import "TargetConditionals.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CALayer (LookinServer)
|
||||
|
||||
/// 如果 myView.layer == myLayer,则 myLayer.lks_hostView 会返回 myView
|
||||
@property(nonatomic, readonly, weak) UIView *lks_hostView;
|
||||
|
||||
- (UIWindow *)lks_window;
|
||||
|
||||
- (CGRect)lks_frameInWindow:(UIWindow *)window;
|
||||
|
||||
- (UIImage *)lks_groupScreenshotWithLowQuality:(BOOL)lowQuality;
|
||||
/// 当没有 sublayers 时,该方法返回 nil
|
||||
- (UIImage *)lks_soloScreenshotWithLowQuality:(BOOL)lowQuality;
|
||||
|
||||
/// 获取和该对象有关的对象的 Class 层级树
|
||||
- (NSArray<NSArray<NSString *> *> *)lks_relatedClassChainList;
|
||||
|
||||
- (NSArray<NSString *> *)lks_selfRelation;
|
||||
|
||||
@property(nonatomic, strong) UIColor *lks_backgroundColor;
|
||||
@property(nonatomic, strong) UIColor *lks_borderColor;
|
||||
@property(nonatomic, strong) UIColor *lks_shadowColor;
|
||||
@property(nonatomic, assign) CGFloat lks_shadowOffsetWidth;
|
||||
@property(nonatomic, assign) CGFloat lks_shadowOffsetHeight;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
233
Pods/LookinServer/Src/Main/Server/Category/CALayer+LookinServer.m
generated
Normal file
233
Pods/LookinServer/Src/Main/Server/Category/CALayer+LookinServer.m
generated
Normal file
@@ -0,0 +1,233 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIView+LookinMobile.m
|
||||
// WeRead
|
||||
//
|
||||
// Created by Li Kai on 2018/11/30.
|
||||
// Copyright © 2018 tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "CALayer+LookinServer.h"
|
||||
#import "LKS_HierarchyDisplayItemsMaker.h"
|
||||
#import "LookinDisplayItem.h"
|
||||
#import <objc/runtime.h>
|
||||
#import "LKS_ConnectionManager.h"
|
||||
#import "LookinIvarTrace.h"
|
||||
#import "LookinServerDefines.h"
|
||||
#import "UIColor+LookinServer.h"
|
||||
#import "LKS_MultiplatformAdapter.h"
|
||||
|
||||
@implementation CALayer (LookinServer)
|
||||
|
||||
- (UIWindow *)lks_window {
|
||||
CALayer *layer = self;
|
||||
while (layer) {
|
||||
UIView *hostView = layer.lks_hostView;
|
||||
if (hostView.window) {
|
||||
return hostView.window;
|
||||
} else if ([hostView isKindOfClass:[UIWindow class]]) {
|
||||
return (UIWindow *)hostView;
|
||||
}
|
||||
layer = layer.superlayer;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGRect)lks_frameInWindow:(UIWindow *)window {
|
||||
UIWindow *selfWindow = [self lks_window];
|
||||
if (!selfWindow) {
|
||||
return CGRectZero;
|
||||
}
|
||||
|
||||
CGRect rectInSelfWindow = [selfWindow.layer convertRect:self.frame fromLayer:self.superlayer];
|
||||
CGRect rectInWindow = [window convertRect:rectInSelfWindow fromWindow:selfWindow];
|
||||
return rectInWindow;
|
||||
}
|
||||
|
||||
#pragma mark - Host View
|
||||
|
||||
- (UIView *)lks_hostView {
|
||||
if (self.delegate && [self.delegate isKindOfClass:UIView.class]) {
|
||||
UIView *view = (UIView *)self.delegate;
|
||||
if (view.layer == self) {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - Screenshot
|
||||
|
||||
- (UIImage *)lks_groupScreenshotWithLowQuality:(BOOL)lowQuality {
|
||||
|
||||
CGFloat screenScale = [LKS_MultiplatformAdapter mainScreenScale];
|
||||
CGFloat pixelWidth = self.frame.size.width * screenScale;
|
||||
CGFloat pixelHeight = self.frame.size.height * screenScale;
|
||||
if (pixelWidth <= 0 || pixelHeight <= 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGFloat renderScale = lowQuality ? 1 : 0;
|
||||
CGFloat maxLength = MAX(pixelWidth, pixelHeight);
|
||||
if (maxLength > LookinNodeImageMaxLengthInPx) {
|
||||
// 确保最终绘制出的图片长和宽都不能超过 LookinNodeImageMaxLengthInPx
|
||||
// 如果算出的 renderScale 大于 1 则取 1,因为似乎用 1 渲染的速度要比一个别的奇怪的带小数点的数字要更快
|
||||
renderScale = MIN(screenScale * LookinNodeImageMaxLengthInPx / maxLength, 1);
|
||||
}
|
||||
|
||||
CGSize contextSize = self.frame.size;
|
||||
if (contextSize.width <= 0 || contextSize.height <= 0 || contextSize.width > 20000 || contextSize.height > 20000) {
|
||||
NSLog(@"LookinServer - Failed to capture screenshot. Invalid context size: %@ x %@", @(contextSize.width), @(contextSize.height));
|
||||
return nil;
|
||||
}
|
||||
UIGraphicsBeginImageContextWithOptions(contextSize, NO, renderScale);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
if (self.lks_hostView && !self.lks_hostView.lks_isChildrenViewOfTabBar) {
|
||||
[self.lks_hostView drawViewHierarchyInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height) afterScreenUpdates:YES];
|
||||
} else {
|
||||
[self renderInContext:context];
|
||||
}
|
||||
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return image;
|
||||
}
|
||||
|
||||
- (UIImage *)lks_soloScreenshotWithLowQuality:(BOOL)lowQuality {
|
||||
if (!self.sublayers.count) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGFloat screenScale = [LKS_MultiplatformAdapter mainScreenScale];
|
||||
CGFloat pixelWidth = self.frame.size.width * screenScale;
|
||||
CGFloat pixelHeight = self.frame.size.height * screenScale;
|
||||
if (pixelWidth <= 0 || pixelHeight <= 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGFloat renderScale = lowQuality ? 1 : 0;
|
||||
CGFloat maxLength = MAX(pixelWidth, pixelHeight);
|
||||
if (maxLength > LookinNodeImageMaxLengthInPx) {
|
||||
// 确保最终绘制出的图片长和宽都不能超过 LookinNodeImageMaxLengthInPx
|
||||
// 如果算出的 renderScale 大于 1 则取 1,因为似乎用 1 渲染的速度要比一个别的奇怪的带小数点的数字要更快
|
||||
renderScale = MIN(screenScale * LookinNodeImageMaxLengthInPx / maxLength, 1);
|
||||
}
|
||||
|
||||
if (self.sublayers.count) {
|
||||
NSArray<CALayer *> *sublayers = [self.sublayers copy];
|
||||
NSMutableArray<CALayer *> *visibleSublayers = [NSMutableArray arrayWithCapacity:sublayers.count];
|
||||
[sublayers enumerateObjectsUsingBlock:^(__kindof CALayer * _Nonnull sublayer, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
if (!sublayer.hidden) {
|
||||
sublayer.hidden = YES;
|
||||
[visibleSublayers addObject:sublayer];
|
||||
}
|
||||
}];
|
||||
|
||||
CGSize contextSize = self.frame.size;
|
||||
if (contextSize.width <= 0 || contextSize.height <= 0 || contextSize.width > 20000 || contextSize.height > 20000) {
|
||||
NSLog(@"LookinServer - Failed to capture screenshot. Invalid context size: %@ x %@", @(contextSize.width), @(contextSize.height));
|
||||
return nil;
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(contextSize, NO, renderScale);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
if (self.lks_hostView && !self.lks_hostView.lks_isChildrenViewOfTabBar) {
|
||||
[self.lks_hostView drawViewHierarchyInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height) afterScreenUpdates:YES];
|
||||
} else {
|
||||
[self renderInContext:context];
|
||||
}
|
||||
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
[visibleSublayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull sublayer, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
sublayer.hidden = NO;
|
||||
}];
|
||||
|
||||
return image;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray<NSArray<NSString *> *> *)lks_relatedClassChainList {
|
||||
NSMutableArray *array = [NSMutableArray arrayWithCapacity:2];
|
||||
if (self.lks_hostView) {
|
||||
[array addObject:[CALayer lks_getClassListOfObject:self.lks_hostView endingClass:@"UIView"]];
|
||||
UIViewController* vc = [self.lks_hostView lks_findHostViewController];
|
||||
if (vc) {
|
||||
[array addObject:[CALayer lks_getClassListOfObject:vc endingClass:@"UIViewController"]];
|
||||
}
|
||||
} else {
|
||||
[array addObject:[CALayer lks_getClassListOfObject:self endingClass:@"CALayer"]];
|
||||
}
|
||||
return array.copy;
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)lks_getClassListOfObject:(id)object endingClass:(NSString *)endingClass {
|
||||
NSArray<NSString *> *completedList = [object lks_classChainList];
|
||||
NSUInteger endingIdx = [completedList indexOfObject:endingClass];
|
||||
if (endingIdx != NSNotFound) {
|
||||
completedList = [completedList subarrayWithRange:NSMakeRange(0, endingIdx + 1)];
|
||||
}
|
||||
return completedList;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)lks_selfRelation {
|
||||
NSMutableArray *array = [NSMutableArray array];
|
||||
NSMutableArray<LookinIvarTrace *> *ivarTraces = [NSMutableArray array];
|
||||
if (self.lks_hostView) {
|
||||
UIViewController* vc = [self.lks_hostView lks_findHostViewController];
|
||||
if (vc) {
|
||||
[array addObject:[NSString stringWithFormat:@"(%@ *).view", NSStringFromClass(vc.class)]];
|
||||
|
||||
[ivarTraces addObjectsFromArray:vc.lks_ivarTraces];
|
||||
}
|
||||
[ivarTraces addObjectsFromArray:self.lks_hostView.lks_ivarTraces];
|
||||
} else {
|
||||
[ivarTraces addObjectsFromArray:self.lks_ivarTraces];
|
||||
}
|
||||
if (ivarTraces.count) {
|
||||
[array addObjectsFromArray:[ivarTraces lookin_map:^id(NSUInteger idx, LookinIvarTrace *value) {
|
||||
return [NSString stringWithFormat:@"(%@ *) -> %@", value.hostClassName, value.ivarName];
|
||||
}]];
|
||||
}
|
||||
return array.count ? array.copy : nil;
|
||||
}
|
||||
|
||||
- (UIColor *)lks_backgroundColor {
|
||||
return [UIColor lks_colorWithCGColor:self.backgroundColor];
|
||||
}
|
||||
- (void)setLks_backgroundColor:(UIColor *)lks_backgroundColor {
|
||||
self.backgroundColor = lks_backgroundColor.CGColor;
|
||||
}
|
||||
|
||||
- (UIColor *)lks_borderColor {
|
||||
return [UIColor lks_colorWithCGColor:self.borderColor];
|
||||
}
|
||||
- (void)setLks_borderColor:(UIColor *)lks_borderColor {
|
||||
self.borderColor = lks_borderColor.CGColor;
|
||||
}
|
||||
|
||||
- (UIColor *)lks_shadowColor {
|
||||
return [UIColor lks_colorWithCGColor:self.shadowColor];
|
||||
}
|
||||
- (void)setLks_shadowColor:(UIColor *)lks_shadowColor {
|
||||
self.shadowColor = lks_shadowColor.CGColor;
|
||||
}
|
||||
|
||||
- (CGFloat)lks_shadowOffsetWidth {
|
||||
return self.shadowOffset.width;
|
||||
}
|
||||
- (void)setLks_shadowOffsetWidth:(CGFloat)lks_shadowOffsetWidth {
|
||||
self.shadowOffset = CGSizeMake(lks_shadowOffsetWidth, self.shadowOffset.height);
|
||||
}
|
||||
|
||||
- (CGFloat)lks_shadowOffsetHeight {
|
||||
return self.shadowOffset.height;
|
||||
}
|
||||
- (void)setLks_shadowOffsetHeight:(CGFloat)lks_shadowOffsetHeight {
|
||||
self.shadowOffset = CGSizeMake(self.shadowOffset.width, lks_shadowOffsetHeight);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
41
Pods/LookinServer/Src/Main/Server/Category/NSObject+LookinServer.h
generated
Normal file
41
Pods/LookinServer/Src/Main/Server/Category/NSObject+LookinServer.h
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// NSObject+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/4/21.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "LookinDefines.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class LookinIvarTrace;
|
||||
|
||||
@interface NSObject (LookinServer)
|
||||
|
||||
#pragma mark - oid
|
||||
|
||||
/// 如果 oid 不存在则会创建新的 oid
|
||||
- (unsigned long)lks_registerOid;
|
||||
|
||||
/// 0 表示不存在
|
||||
@property(nonatomic, assign) unsigned long lks_oid;
|
||||
|
||||
+ (NSObject *)lks_objectWithOid:(unsigned long)oid;
|
||||
|
||||
#pragma mark - trace
|
||||
|
||||
@property(nonatomic, copy) NSString *lks_specialTrace;
|
||||
|
||||
+ (void)lks_clearAllObjectsTraces;
|
||||
|
||||
/**
|
||||
获取当前对象的 Class 层级树,如 @[@"UIView", @"UIResponder", @"NSObject"]。未 demangle,有 Swift Module Name
|
||||
*/
|
||||
- (NSArray<NSString *> *)lks_classChainList;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
99
Pods/LookinServer/Src/Main/Server/Category/NSObject+LookinServer.m
generated
Normal file
99
Pods/LookinServer/Src/Main/Server/Category/NSObject+LookinServer.m
generated
Normal file
@@ -0,0 +1,99 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// NSObject+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/4/21.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "NSObject+Lookin.h"
|
||||
#import "NSObject+LookinServer.h"
|
||||
#import "LookinServerDefines.h"
|
||||
#import "LKS_ObjectRegistry.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation NSObject (LookinServer)
|
||||
|
||||
#pragma mark - oid
|
||||
|
||||
- (unsigned long)lks_registerOid {
|
||||
if (!self.lks_oid) {
|
||||
unsigned long oid = [[LKS_ObjectRegistry sharedInstance] addObject:self];
|
||||
self.lks_oid = oid;
|
||||
}
|
||||
return self.lks_oid;
|
||||
}
|
||||
|
||||
- (void)setLks_oid:(unsigned long)lks_oid {
|
||||
[self lookin_bindObject:@(lks_oid) forKey:@"lks_oid"];
|
||||
}
|
||||
|
||||
- (unsigned long)lks_oid {
|
||||
NSNumber *number = [self lookin_getBindObjectForKey:@"lks_oid"];
|
||||
return [number unsignedLongValue];
|
||||
}
|
||||
|
||||
+ (NSObject *)lks_objectWithOid:(unsigned long)oid {
|
||||
return [[LKS_ObjectRegistry sharedInstance] objectWithOid:oid];
|
||||
}
|
||||
|
||||
#pragma mark - trace
|
||||
|
||||
- (void)setLks_ivarTraces:(NSArray<LookinIvarTrace *> *)lks_ivarTraces {
|
||||
[self lookin_bindObject:lks_ivarTraces.copy forKey:@"lks_ivarTraces"];
|
||||
|
||||
if (lks_ivarTraces) {
|
||||
[[NSObject lks_allObjectsWithTraces] addPointer:(void *)self];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<LookinIvarTrace *> *)lks_ivarTraces {
|
||||
return [self lookin_getBindObjectForKey:@"lks_ivarTraces"];
|
||||
}
|
||||
|
||||
- (void)setLks_specialTrace:(NSString *)lks_specialTrace {
|
||||
[self lookin_bindObject:lks_specialTrace forKey:@"lks_specialTrace"];
|
||||
if (lks_specialTrace) {
|
||||
[[NSObject lks_allObjectsWithTraces] addPointer:(void *)self];
|
||||
}
|
||||
}
|
||||
- (NSString *)lks_specialTrace {
|
||||
return [self lookin_getBindObjectForKey:@"lks_specialTrace"];
|
||||
}
|
||||
|
||||
+ (void)lks_clearAllObjectsTraces {
|
||||
[[[NSObject lks_allObjectsWithTraces] allObjects] enumerateObjectsUsingBlock:^(NSObject * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
obj.lks_ivarTraces = nil;
|
||||
obj.lks_specialTrace = nil;
|
||||
}];
|
||||
[NSObject lks_allObjectsWithTraces].count = 0;
|
||||
}
|
||||
|
||||
+ (NSPointerArray *)lks_allObjectsWithTraces {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSPointerArray *lks_allObjectsWithTraces = nil;
|
||||
dispatch_once(&onceToken,^{
|
||||
lks_allObjectsWithTraces = [NSPointerArray weakObjectsPointerArray];
|
||||
});
|
||||
return lks_allObjectsWithTraces;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)lks_classChainList {
|
||||
NSMutableArray<NSString *> *classChainList = [NSMutableArray array];
|
||||
Class currentClass = self.class;
|
||||
|
||||
while (currentClass) {
|
||||
NSString *currentClassName = NSStringFromClass(currentClass);
|
||||
if (currentClassName) {
|
||||
[classChainList addObject:currentClassName];
|
||||
}
|
||||
currentClass = [currentClass superclass];
|
||||
}
|
||||
return classChainList.copy;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
21
Pods/LookinServer/Src/Main/Server/Category/UIBlurEffect+LookinServer.h
generated
Normal file
21
Pods/LookinServer/Src/Main/Server/Category/UIBlurEffect+LookinServer.h
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIBlurEffect+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/10/8.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIBlurEffect (LookinServer)
|
||||
|
||||
/// 该 number 包装的对象是 UIBlurEffectStyle,之所以用 NSNumber 是因为想把 0 和 nil 区分开,毕竟这里是在 hook 系统,稳一点好。
|
||||
/// 该方法的实现需要 Hook,因此若定义了 LOOKIN_SERVER_DISABLE_HOOK 宏,则属性会返回 nil
|
||||
@property(nonatomic, strong) NSNumber *lks_effectStyleNumber;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
57
Pods/LookinServer/Src/Main/Server/Category/UIBlurEffect+LookinServer.m
generated
Normal file
57
Pods/LookinServer/Src/Main/Server/Category/UIBlurEffect+LookinServer.m
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIBlurEffect+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/10/8.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UIBlurEffect+LookinServer.h"
|
||||
#import "NSObject+Lookin.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation UIBlurEffect (LookinServer)
|
||||
|
||||
#ifdef LOOKIN_SERVER_DISABLE_HOOK
|
||||
|
||||
- (void)setLks_effectStyleNumber:(NSNumber *)lks_effectStyleNumber {
|
||||
}
|
||||
|
||||
- (NSNumber *)lks_effectStyleNumber {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
+ (void)load {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
Method oriMethod = class_getClassMethod([self class], @selector(effectWithStyle:));
|
||||
Method newMethod = class_getClassMethod([self class], @selector(lks_effectWithStyle:));
|
||||
method_exchangeImplementations(oriMethod, newMethod);
|
||||
});
|
||||
}
|
||||
|
||||
+ (UIBlurEffect *)lks_effectWithStyle:(UIBlurEffectStyle)style {
|
||||
id effect = [self lks_effectWithStyle:style];
|
||||
if ([effect respondsToSelector:@selector(setLks_effectStyleNumber:)]) {
|
||||
[effect setLks_effectStyleNumber:@(style)];
|
||||
}
|
||||
return effect;
|
||||
}
|
||||
|
||||
- (void)setLks_effectStyleNumber:(NSNumber *)lks_effectStyleNumber {
|
||||
[self lookin_bindObject:lks_effectStyleNumber forKey:@"lks_effectStyleNumber"];
|
||||
}
|
||||
|
||||
- (NSNumber *)lks_effectStyleNumber {
|
||||
return [self lookin_getBindObjectForKey:@"lks_effectStyleNumber"];
|
||||
}
|
||||
|
||||
#endif /* LOOKIN_SERVER_DISABLE_HOOK */
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
26
Pods/LookinServer/Src/Main/Server/Category/UIColor+LookinServer.h
generated
Normal file
26
Pods/LookinServer/Src/Main/Server/Category/UIColor+LookinServer.h
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIColor+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/6/5.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIColor (LookinServer)
|
||||
|
||||
- (NSArray<NSNumber *> *)lks_rgbaComponents;
|
||||
+ (instancetype)lks_colorFromRGBAComponents:(NSArray<NSNumber *> *)components;
|
||||
|
||||
- (NSString *)lks_rgbaString;
|
||||
- (NSString *)lks_hexString;
|
||||
|
||||
/// will check if the argument is a real CGColor
|
||||
+ (UIColor *)lks_colorWithCGColor:(CGColorRef)cgColor;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
183
Pods/LookinServer/Src/Main/Server/Category/UIColor+LookinServer.m
generated
Normal file
183
Pods/LookinServer/Src/Main/Server/Category/UIColor+LookinServer.m
generated
Normal file
@@ -0,0 +1,183 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIColor+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/6/5.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UIColor+LookinServer.h"
|
||||
|
||||
@implementation UIColor (LookinServer)
|
||||
|
||||
- (NSArray<NSNumber *> *)lks_rgbaComponents {
|
||||
CGFloat r, g, b, a;
|
||||
CGColorRef cgColor = [self CGColor];
|
||||
const CGFloat *components = CGColorGetComponents(cgColor);
|
||||
if (CGColorGetNumberOfComponents(cgColor) == 4) {
|
||||
r = components[0];
|
||||
g = components[1];
|
||||
b = components[2];
|
||||
a = components[3];
|
||||
} else if (CGColorGetNumberOfComponents(cgColor) == 2) {
|
||||
r = components[0];
|
||||
g = components[0];
|
||||
b = components[0];
|
||||
a = components[1];
|
||||
} else if (CGColorGetNumberOfComponents(cgColor) == 1) {
|
||||
r = components[0];
|
||||
g = components[0];
|
||||
b = components[0];
|
||||
a = components[0];
|
||||
} else {
|
||||
r = 0;
|
||||
g = 0;
|
||||
b = 0;
|
||||
a = 0;
|
||||
NSAssert(NO, @"");
|
||||
}
|
||||
NSArray<NSNumber *> *rgba = @[@(r), @(g), @(b), @(a)];
|
||||
return rgba;
|
||||
}
|
||||
|
||||
+ (instancetype)lks_colorFromRGBAComponents:(NSArray<NSNumber *> *)components {
|
||||
if (!components) {
|
||||
return nil;
|
||||
}
|
||||
if (components.count != 4) {
|
||||
NSAssert(NO, @"");
|
||||
return nil;
|
||||
}
|
||||
UIColor *color = [UIColor colorWithRed:components[0].doubleValue green:components[1].doubleValue blue:components[2].doubleValue alpha:components[3].doubleValue];
|
||||
return color;
|
||||
}
|
||||
|
||||
- (NSString *)lks_rgbaString {
|
||||
CGFloat r, g, b, a;
|
||||
CGColorRef cgColor = [self CGColor];
|
||||
const CGFloat *components = CGColorGetComponents(cgColor);
|
||||
if (CGColorGetNumberOfComponents(cgColor) == 4) {
|
||||
r = components[0];
|
||||
g = components[1];
|
||||
b = components[2];
|
||||
a = components[3];
|
||||
} else if (CGColorGetNumberOfComponents(cgColor) == 2) {
|
||||
r = components[0];
|
||||
g = components[0];
|
||||
b = components[0];
|
||||
a = components[1];
|
||||
} else {
|
||||
r = 0;
|
||||
g = 0;
|
||||
b = 0;
|
||||
a = 0;
|
||||
NSAssert(NO, @"");
|
||||
}
|
||||
|
||||
if (a >= 1) {
|
||||
return [NSString stringWithFormat:@"(%.0f, %.0f, %.0f)", r * 255, g * 255, b * 255];
|
||||
} else {
|
||||
return [NSString stringWithFormat:@"(%.0f, %.0f, %.0f, %.2f)", r * 255, g * 255, b * 255, a];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)lks_hexString {
|
||||
CGFloat r, g, b, a;
|
||||
CGColorRef cgColor = [self CGColor];
|
||||
const CGFloat *components = CGColorGetComponents(cgColor);
|
||||
if (CGColorGetNumberOfComponents(cgColor) == 4) {
|
||||
r = components[0];
|
||||
g = components[1];
|
||||
b = components[2];
|
||||
a = components[3];
|
||||
} else if (CGColorGetNumberOfComponents(cgColor) == 2) {
|
||||
r = components[0];
|
||||
g = components[0];
|
||||
b = components[0];
|
||||
a = components[1];
|
||||
} else {
|
||||
r = 0;
|
||||
g = 0;
|
||||
b = 0;
|
||||
a = 0;
|
||||
NSAssert(NO, @"");
|
||||
}
|
||||
|
||||
NSInteger red = r * 255;
|
||||
NSInteger green = g * 255;
|
||||
NSInteger blue = b * 255;
|
||||
NSInteger alpha = a * 255;
|
||||
|
||||
return [[NSString stringWithFormat:@"#%@%@%@%@",
|
||||
[UIColor _alignColorHexStringLength:[UIColor _hexStringWithInteger:alpha]],
|
||||
[UIColor _alignColorHexStringLength:[UIColor _hexStringWithInteger:red]],
|
||||
[UIColor _alignColorHexStringLength:[UIColor _hexStringWithInteger:green]],
|
||||
[UIColor _alignColorHexStringLength:[UIColor _hexStringWithInteger:blue]]] lowercaseString];
|
||||
}
|
||||
|
||||
// 对于色值只有单位数的,在前面补一个0,例如“F”会补齐为“0F”
|
||||
+ (NSString *)_alignColorHexStringLength:(NSString *)hexString {
|
||||
return hexString.length < 2 ? [@"0" stringByAppendingString:hexString] : hexString;
|
||||
}
|
||||
|
||||
+ (NSString *)_hexStringWithInteger:(NSInteger)integer {
|
||||
NSString *hexString = @"";
|
||||
NSInteger remainder = 0;
|
||||
for (NSInteger i = 0; i < 9; i++) {
|
||||
remainder = integer % 16;
|
||||
integer = integer / 16;
|
||||
NSString *letter = [self _hexLetterStringWithInteger:remainder];
|
||||
hexString = [letter stringByAppendingString:hexString];
|
||||
if (integer == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return hexString;
|
||||
}
|
||||
|
||||
+ (NSString *)_hexLetterStringWithInteger:(NSInteger)integer {
|
||||
NSAssert(integer < 16, @"要转换的数必须是16进制里的个位数,也即小于16,但你传给我是%@", @(integer));
|
||||
|
||||
NSString *letter = nil;
|
||||
switch (integer) {
|
||||
case 10:
|
||||
letter = @"A";
|
||||
break;
|
||||
case 11:
|
||||
letter = @"B";
|
||||
break;
|
||||
case 12:
|
||||
letter = @"C";
|
||||
break;
|
||||
case 13:
|
||||
letter = @"D";
|
||||
break;
|
||||
case 14:
|
||||
letter = @"E";
|
||||
break;
|
||||
case 15:
|
||||
letter = @"F";
|
||||
break;
|
||||
default:
|
||||
letter = [[NSString alloc]initWithFormat:@"%@", @(integer)];
|
||||
break;
|
||||
}
|
||||
return letter;
|
||||
}
|
||||
|
||||
+ (UIColor *)lks_colorWithCGColor:(CGColorRef)cgColor {
|
||||
if (!cgColor) {
|
||||
return nil;
|
||||
}
|
||||
if (CFGetTypeID(cgColor) != CGColorGetTypeID()) {
|
||||
return nil;
|
||||
}
|
||||
return [UIColor colorWithCGColor:cgColor];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
22
Pods/LookinServer/Src/Main/Server/Category/UIImage+LookinServer.h
generated
Normal file
22
Pods/LookinServer/Src/Main/Server/Category/UIImage+LookinServer.h
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIImage+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/5/14.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIImage (LookinServer)
|
||||
|
||||
/// 该方法的实现需要 Hook,因此若定义了 LOOKIN_SERVER_DISABLE_HOOK 宏,则属性会返回 nil
|
||||
@property(nonatomic, copy) NSString *lks_imageSourceName;
|
||||
|
||||
- (NSData *)lookin_data;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
95
Pods/LookinServer/Src/Main/Server/Category/UIImage+LookinServer.m
generated
Normal file
95
Pods/LookinServer/Src/Main/Server/Category/UIImage+LookinServer.m
generated
Normal file
@@ -0,0 +1,95 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIImage+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/5/14.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <objc/runtime.h>
|
||||
#import "UIImage+LookinServer.h"
|
||||
#import "LookinServerDefines.h"
|
||||
|
||||
@implementation UIImage (LookinServer)
|
||||
|
||||
#ifdef LOOKIN_SERVER_DISABLE_HOOK
|
||||
|
||||
- (void)setLks_imageSourceName:(NSString *)lks_imageSourceName {
|
||||
}
|
||||
|
||||
- (NSString *)lks_imageSourceName {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
+ (void)load {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
Method oriMethod = class_getClassMethod([self class], @selector(imageNamed:));
|
||||
Method newMethod = class_getClassMethod([self class], @selector(lks_imageNamed:));
|
||||
method_exchangeImplementations(oriMethod, newMethod);
|
||||
|
||||
oriMethod = class_getClassMethod([self class], @selector(imageWithContentsOfFile:));
|
||||
newMethod = class_getClassMethod([self class], @selector(lks_imageWithContentsOfFile:));
|
||||
method_exchangeImplementations(oriMethod, newMethod);
|
||||
|
||||
oriMethod = class_getClassMethod([self class], @selector(imageNamed:inBundle:compatibleWithTraitCollection:));
|
||||
newMethod = class_getClassMethod([self class], @selector(lks_imageNamed:inBundle:compatibleWithTraitCollection:));
|
||||
method_exchangeImplementations(oriMethod, newMethod);
|
||||
|
||||
if (@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)) {
|
||||
oriMethod = class_getClassMethod([self class], @selector(imageNamed:inBundle:withConfiguration:));
|
||||
newMethod = class_getClassMethod([self class], @selector(lks_imageNamed:inBundle:withConfiguration:));
|
||||
method_exchangeImplementations(oriMethod, newMethod);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+ (nullable UIImage *)lks_imageNamed:(NSString *)name inBundle:(nullable NSBundle *)bundle withConfiguration:(nullable UIImageConfiguration *)configuration API_AVAILABLE(ios(13.0),tvos(13.0),watchos(6.0))
|
||||
{
|
||||
UIImage *image = [self lks_imageNamed:name inBundle:bundle withConfiguration:configuration];
|
||||
image.lks_imageSourceName = name;
|
||||
return image;
|
||||
}
|
||||
|
||||
+ (nullable UIImage *)lks_imageNamed:(NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection API_AVAILABLE(ios(8.0))
|
||||
{
|
||||
UIImage *image = [self lks_imageNamed:name inBundle:bundle compatibleWithTraitCollection:traitCollection];
|
||||
image.lks_imageSourceName = name;
|
||||
return image;
|
||||
}
|
||||
|
||||
+ (UIImage *)lks_imageNamed:(NSString *)name {
|
||||
UIImage *image = [self lks_imageNamed:name];
|
||||
image.lks_imageSourceName = name;
|
||||
return image;
|
||||
}
|
||||
|
||||
+ (UIImage *)lks_imageWithContentsOfFile:(NSString *)path {
|
||||
UIImage *image = [self lks_imageWithContentsOfFile:path];
|
||||
|
||||
NSString *fileName = [[path componentsSeparatedByString:@"/"].lastObject componentsSeparatedByString:@"."].firstObject;
|
||||
image.lks_imageSourceName = fileName;
|
||||
return image;
|
||||
}
|
||||
|
||||
- (void)setLks_imageSourceName:(NSString *)lks_imageSourceName {
|
||||
[self lookin_bindObject:lks_imageSourceName.copy forKey:@"lks_imageSourceName"];
|
||||
}
|
||||
|
||||
- (NSString *)lks_imageSourceName {
|
||||
return [self lookin_getBindObjectForKey:@"lks_imageSourceName"];
|
||||
}
|
||||
|
||||
#endif /* LOOKIN_SERVER_DISABLE_HOOK */
|
||||
|
||||
- (NSData *)lookin_data {
|
||||
return UIImagePNGRepresentation(self);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
20
Pods/LookinServer/Src/Main/Server/Category/UIImageView+LookinServer.h
generated
Normal file
20
Pods/LookinServer/Src/Main/Server/Category/UIImageView+LookinServer.h
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIImageView+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/9/18.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIImageView (LookinServer)
|
||||
|
||||
- (NSString *)lks_imageSourceName;
|
||||
- (NSNumber *)lks_imageViewOidIfHasImage;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
31
Pods/LookinServer/Src/Main/Server/Category/UIImageView+LookinServer.m
generated
Normal file
31
Pods/LookinServer/Src/Main/Server/Category/UIImageView+LookinServer.m
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIImageView+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/9/18.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UIImageView+LookinServer.h"
|
||||
#import "UIImage+LookinServer.h"
|
||||
#import "NSObject+LookinServer.h"
|
||||
|
||||
@implementation UIImageView (LookinServer)
|
||||
|
||||
- (NSString *)lks_imageSourceName {
|
||||
return self.image.lks_imageSourceName;
|
||||
}
|
||||
|
||||
- (NSNumber *)lks_imageViewOidIfHasImage {
|
||||
if (!self.image) {
|
||||
return nil;
|
||||
}
|
||||
unsigned long oid = [self lks_registerOid];
|
||||
return @(oid);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
21
Pods/LookinServer/Src/Main/Server/Category/UILabel+LookinServer.h
generated
Normal file
21
Pods/LookinServer/Src/Main/Server/Category/UILabel+LookinServer.h
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UILabel+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/2/26.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UILabel (LookinServer)
|
||||
|
||||
@property(nonatomic, assign) CGFloat lks_fontSize;
|
||||
|
||||
- (NSString *)lks_fontName;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
29
Pods/LookinServer/Src/Main/Server/Category/UILabel+LookinServer.m
generated
Normal file
29
Pods/LookinServer/Src/Main/Server/Category/UILabel+LookinServer.m
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UILabel+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/2/26.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UILabel+LookinServer.h"
|
||||
|
||||
@implementation UILabel (LookinServer)
|
||||
|
||||
- (CGFloat)lks_fontSize {
|
||||
return self.font.pointSize;
|
||||
}
|
||||
- (void)setLks_fontSize:(CGFloat)lks_fontSize {
|
||||
UIFont *font = [self.font fontWithSize:lks_fontSize];
|
||||
self.font = font;
|
||||
}
|
||||
|
||||
- (NSString *)lks_fontName {
|
||||
return self.font.fontName;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
19
Pods/LookinServer/Src/Main/Server/Category/UITableView+LookinServer.h
generated
Normal file
19
Pods/LookinServer/Src/Main/Server/Category/UITableView+LookinServer.h
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UITableView+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/9/5.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UITableView (LookinServer)
|
||||
|
||||
- (NSArray<NSNumber *> *)lks_numberOfRows;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
29
Pods/LookinServer/Src/Main/Server/Category/UITableView+LookinServer.m
generated
Normal file
29
Pods/LookinServer/Src/Main/Server/Category/UITableView+LookinServer.m
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UITableView+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/9/5.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UITableView+LookinServer.h"
|
||||
#import "LookinServerDefines.h"
|
||||
|
||||
@implementation UITableView (LookinServer)
|
||||
|
||||
- (NSArray<NSNumber *> *)lks_numberOfRows {
|
||||
NSUInteger sectionsCount = MIN(self.numberOfSections, 10);
|
||||
NSArray<NSNumber *> *rowsCount = [NSArray lookin_arrayWithCount:sectionsCount block:^id(NSUInteger idx) {
|
||||
return @([self numberOfRowsInSection:idx]);
|
||||
}];
|
||||
if (rowsCount.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
return rowsCount;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
21
Pods/LookinServer/Src/Main/Server/Category/UITextField+LookinServer.h
generated
Normal file
21
Pods/LookinServer/Src/Main/Server/Category/UITextField+LookinServer.h
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UITextField+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/2/26.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UITextField (LookinServer)
|
||||
|
||||
@property(nonatomic, assign) CGFloat lks_fontSize;
|
||||
|
||||
- (NSString *)lks_fontName;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
29
Pods/LookinServer/Src/Main/Server/Category/UITextField+LookinServer.m
generated
Normal file
29
Pods/LookinServer/Src/Main/Server/Category/UITextField+LookinServer.m
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UITextField+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/2/26.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UITextField+LookinServer.h"
|
||||
|
||||
@implementation UITextField (LookinServer)
|
||||
|
||||
- (CGFloat)lks_fontSize {
|
||||
return self.font.pointSize;
|
||||
}
|
||||
- (void)setLks_fontSize:(CGFloat)lks_fontSize {
|
||||
UIFont *font = [self.font fontWithSize:lks_fontSize];
|
||||
self.font = font;
|
||||
}
|
||||
|
||||
- (NSString *)lks_fontName {
|
||||
return self.font.fontName;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
21
Pods/LookinServer/Src/Main/Server/Category/UITextView+LookinServer.h
generated
Normal file
21
Pods/LookinServer/Src/Main/Server/Category/UITextView+LookinServer.h
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UITextView+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/2/26.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UITextView (LookinServer)
|
||||
|
||||
@property(nonatomic, assign) CGFloat lks_fontSize;
|
||||
|
||||
- (NSString *)lks_fontName;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
29
Pods/LookinServer/Src/Main/Server/Category/UITextView+LookinServer.m
generated
Normal file
29
Pods/LookinServer/Src/Main/Server/Category/UITextView+LookinServer.m
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UITextView+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/2/26.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UITextView+LookinServer.h"
|
||||
|
||||
@implementation UITextView (LookinServer)
|
||||
|
||||
- (CGFloat)lks_fontSize {
|
||||
return self.font.pointSize;
|
||||
}
|
||||
- (void)setLks_fontSize:(CGFloat)lks_fontSize {
|
||||
UIFont *font = [self.font fontWithSize:lks_fontSize];
|
||||
self.font = font;
|
||||
}
|
||||
|
||||
- (NSString *)lks_fontName {
|
||||
return self.font.fontName;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
44
Pods/LookinServer/Src/Main/Server/Category/UIView+LookinServer.h
generated
Normal file
44
Pods/LookinServer/Src/Main/Server/Category/UIView+LookinServer.h
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIView+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/3/19.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "LookinDefines.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIView (LookinServer)
|
||||
|
||||
/// 如果 myViewController.view = myView,则可以通过 myView 的 lks_findHostViewController 方法找到 myViewController
|
||||
- (UIViewController *)lks_findHostViewController;
|
||||
|
||||
/// 是否是 UITabBar 的 childrenView,如果是的话,则截图时需要强制使用 renderInContext: 的方式而非 drawViewHierarchyInRect:afterScreenUpdates: 否则在 iOS 13 上获取到的图像是空的不知道为什么
|
||||
@property(nonatomic, assign) BOOL lks_isChildrenViewOfTabBar;
|
||||
|
||||
/// point 是相对于 receiver 自身的坐标系
|
||||
- (UIView *)lks_subviewAtPoint:(CGPoint)point preferredClasses:(NSArray<Class> *)preferredClasses;
|
||||
|
||||
- (CGFloat)lks_bestWidth;
|
||||
- (CGFloat)lks_bestHeight;
|
||||
- (CGSize)lks_bestSize;
|
||||
|
||||
@property(nonatomic, assign) float lks_horizontalContentHuggingPriority;
|
||||
@property(nonatomic, assign) float lks_verticalContentHuggingPriority;
|
||||
|
||||
@property(nonatomic, assign) float lks_horizontalContentCompressionResistancePriority;
|
||||
@property(nonatomic, assign) float lks_verticalContentCompressionResistancePriority;
|
||||
|
||||
/// 遍历全局的 view 并给他们设置 lks_involvedRawConstraints 属性
|
||||
+ (void)lks_rebuildGlobalInvolvedRawConstraints;
|
||||
/// 该属性保存了牵扯到当前 view 的所有 constraints,包括那些没有生效的
|
||||
@property(nonatomic, strong) NSMutableArray<NSLayoutConstraint *> *lks_involvedRawConstraints;
|
||||
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)lks_constraints;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
215
Pods/LookinServer/Src/Main/Server/Category/UIView+LookinServer.m
generated
Normal file
215
Pods/LookinServer/Src/Main/Server/Category/UIView+LookinServer.m
generated
Normal file
@@ -0,0 +1,215 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIView+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/3/19.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UIView+LookinServer.h"
|
||||
#import <objc/runtime.h>
|
||||
#import "LookinObject.h"
|
||||
#import "LookinAutoLayoutConstraint.h"
|
||||
#import "LookinServerDefines.h"
|
||||
#import "LKS_MultiplatformAdapter.h"
|
||||
|
||||
@implementation UIView (LookinServer)
|
||||
|
||||
- (UIViewController *)lks_findHostViewController {
|
||||
UIResponder *responder = [self nextResponder];
|
||||
if (!responder) {
|
||||
return nil;
|
||||
}
|
||||
if (![responder isKindOfClass:[UIViewController class]]) {
|
||||
return nil;
|
||||
}
|
||||
UIViewController *viewController = (UIViewController *)responder;
|
||||
if (viewController.view != self) {
|
||||
return nil;
|
||||
}
|
||||
return viewController;
|
||||
}
|
||||
|
||||
- (UIView *)lks_subviewAtPoint:(CGPoint)point preferredClasses:(NSArray<Class> *)preferredClasses {
|
||||
BOOL isPreferredClassForSelf = [preferredClasses lookin_any:^BOOL(Class obj) {
|
||||
return [self isKindOfClass:obj];
|
||||
}];
|
||||
if (isPreferredClassForSelf) {
|
||||
return self;
|
||||
}
|
||||
|
||||
UIView *targetView = [self.subviews lookin_lastFiltered:^BOOL(__kindof UIView *obj) {
|
||||
if (obj.hidden || obj.alpha <= 0.01) {
|
||||
return NO;
|
||||
}
|
||||
BOOL contains = CGRectContainsPoint(obj.frame, point);
|
||||
return contains;
|
||||
}];
|
||||
|
||||
if (!targetView) {
|
||||
return self;
|
||||
}
|
||||
|
||||
CGPoint newPoint = [targetView convertPoint:point fromView:self];
|
||||
targetView = [targetView lks_subviewAtPoint:newPoint preferredClasses:preferredClasses];
|
||||
return targetView;
|
||||
}
|
||||
|
||||
- (CGSize)lks_bestSize {
|
||||
return [self sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
|
||||
}
|
||||
|
||||
- (CGFloat)lks_bestWidth {
|
||||
return self.lks_bestSize.width;
|
||||
}
|
||||
|
||||
- (CGFloat)lks_bestHeight {
|
||||
return self.lks_bestSize.height;
|
||||
}
|
||||
|
||||
- (void)setLks_isChildrenViewOfTabBar:(BOOL)lks_isChildrenViewOfTabBar {
|
||||
[self lookin_bindBOOL:lks_isChildrenViewOfTabBar forKey:@"lks_isChildrenViewOfTabBar"];
|
||||
}
|
||||
- (BOOL)lks_isChildrenViewOfTabBar {
|
||||
return [self lookin_getBindBOOLForKey:@"lks_isChildrenViewOfTabBar"];
|
||||
}
|
||||
|
||||
- (void)setLks_verticalContentHuggingPriority:(float)lks_verticalContentHuggingPriority {
|
||||
[self setContentHuggingPriority:lks_verticalContentHuggingPriority forAxis:UILayoutConstraintAxisVertical];
|
||||
}
|
||||
- (float)lks_verticalContentHuggingPriority {
|
||||
return [self contentHuggingPriorityForAxis:UILayoutConstraintAxisVertical];
|
||||
}
|
||||
|
||||
- (void)setLks_horizontalContentHuggingPriority:(float)lks_horizontalContentHuggingPriority {
|
||||
[self setContentHuggingPriority:lks_horizontalContentHuggingPriority forAxis:UILayoutConstraintAxisHorizontal];
|
||||
}
|
||||
- (float)lks_horizontalContentHuggingPriority {
|
||||
return [self contentHuggingPriorityForAxis:UILayoutConstraintAxisHorizontal];
|
||||
}
|
||||
|
||||
- (void)setLks_verticalContentCompressionResistancePriority:(float)lks_verticalContentCompressionResistancePriority {
|
||||
[self setContentCompressionResistancePriority:lks_verticalContentCompressionResistancePriority forAxis:UILayoutConstraintAxisVertical];
|
||||
}
|
||||
- (float)lks_verticalContentCompressionResistancePriority {
|
||||
return [self contentCompressionResistancePriorityForAxis:UILayoutConstraintAxisVertical];
|
||||
}
|
||||
|
||||
- (void)setLks_horizontalContentCompressionResistancePriority:(float)lks_horizontalContentCompressionResistancePriority {
|
||||
[self setContentCompressionResistancePriority:lks_horizontalContentCompressionResistancePriority forAxis:UILayoutConstraintAxisHorizontal];
|
||||
}
|
||||
- (float)lks_horizontalContentCompressionResistancePriority {
|
||||
return [self contentCompressionResistancePriorityForAxis:UILayoutConstraintAxisHorizontal];
|
||||
}
|
||||
|
||||
+ (void)lks_rebuildGlobalInvolvedRawConstraints {
|
||||
[[LKS_MultiplatformAdapter allWindows] enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
[self lks_removeInvolvedRawConstraintsForViewsRootedByView:window];
|
||||
}];
|
||||
[[LKS_MultiplatformAdapter allWindows] enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
[self lks_addInvolvedRawConstraintsForViewsRootedByView:window];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)lks_addInvolvedRawConstraintsForViewsRootedByView:(UIView *)rootView {
|
||||
[rootView.constraints enumerateObjectsUsingBlock:^(__kindof NSLayoutConstraint * _Nonnull constraint, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
UIView *firstView = constraint.firstItem;
|
||||
if ([firstView isKindOfClass:[UIView class]] && ![firstView.lks_involvedRawConstraints containsObject:constraint]) {
|
||||
if (!firstView.lks_involvedRawConstraints) {
|
||||
firstView.lks_involvedRawConstraints = [NSMutableArray array];
|
||||
}
|
||||
[firstView.lks_involvedRawConstraints addObject:constraint];
|
||||
}
|
||||
|
||||
UIView *secondView = constraint.secondItem;
|
||||
if ([secondView isKindOfClass:[UIView class]] && ![secondView.lks_involvedRawConstraints containsObject:constraint]) {
|
||||
if (!secondView.lks_involvedRawConstraints) {
|
||||
secondView.lks_involvedRawConstraints = [NSMutableArray array];
|
||||
}
|
||||
[secondView.lks_involvedRawConstraints addObject:constraint];
|
||||
}
|
||||
}];
|
||||
|
||||
[rootView.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
[self lks_addInvolvedRawConstraintsForViewsRootedByView:subview];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)lks_removeInvolvedRawConstraintsForViewsRootedByView:(UIView *)rootView {
|
||||
[rootView.lks_involvedRawConstraints removeAllObjects];
|
||||
[rootView.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
[self lks_removeInvolvedRawConstraintsForViewsRootedByView:subview];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)setLks_involvedRawConstraints:(NSMutableArray<NSLayoutConstraint *> *)lks_involvedRawConstraints {
|
||||
[self lookin_bindObject:lks_involvedRawConstraints forKey:@"lks_involvedRawConstraints"];
|
||||
}
|
||||
|
||||
- (NSMutableArray<NSLayoutConstraint *> *)lks_involvedRawConstraints {
|
||||
return [self lookin_getBindObjectForKey:@"lks_involvedRawConstraints"];
|
||||
}
|
||||
|
||||
- (NSArray<LookinAutoLayoutConstraint *> *)lks_constraints {
|
||||
/**
|
||||
- lks_involvedRawConstraints 保存了牵扯到了 self 的所有的 constraints(包括未生效的,但不包括 inactive 的,整个产品逻辑都是直接忽略 inactive 的 constraints)
|
||||
- 通过 constraintsAffectingLayoutForAxis 可以拿到会影响 self 布局的所有已生效的 constraints(这里称之为 effectiveConstraints)
|
||||
- 很多情况下,一条 constraint 会出现在 effectiveConstraints 里但不会出现在 lks_involvedRawConstraints 里,比如:
|
||||
· UIWindow 拥有 minX, minY, width, height 四个 effectiveConstraints,但 lks_involvedRawConstraints 为空,因为它的 constraints 属性为空(这一点不知道为啥,但 Xcode Inspector 和 Reveal 确实也不会显示这四个 constraints)
|
||||
· 如果设置了 View1 的 center 和 superview 的 center 保持一致,则 superview 的 width 和 height 也会出现在 effectiveConstraints 里,但不会出现在 lks_involvedRawConstraints 里(这点可以理解,毕竟这种场景下 superview 的 width 和 height 确实会影响到 View1)
|
||||
*/
|
||||
NSMutableArray<NSLayoutConstraint *> *effectiveConstraints = [NSMutableArray array];
|
||||
[effectiveConstraints addObjectsFromArray:[self constraintsAffectingLayoutForAxis:UILayoutConstraintAxisHorizontal]];
|
||||
[effectiveConstraints addObjectsFromArray:[self constraintsAffectingLayoutForAxis:UILayoutConstraintAxisVertical]];
|
||||
|
||||
NSArray<LookinAutoLayoutConstraint *> *lookinConstraints = [self.lks_involvedRawConstraints lookin_map:^id(NSUInteger idx, __kindof NSLayoutConstraint *constraint) {
|
||||
BOOL isEffective = [effectiveConstraints containsObject:constraint];
|
||||
if ([constraint isActive]) {
|
||||
// trying to get firstItem or secondItem of an inactive constraint may cause dangling-pointer crash
|
||||
// https://github.com/QMUI/LookinServer/issues/86
|
||||
LookinConstraintItemType firstItemType = [self _lks_constraintItemTypeForItem:constraint.firstItem];
|
||||
LookinConstraintItemType secondItemType = [self _lks_constraintItemTypeForItem:constraint.secondItem];
|
||||
LookinAutoLayoutConstraint *lookinConstraint = [LookinAutoLayoutConstraint instanceFromNSConstraint:constraint isEffective:isEffective firstItemType:firstItemType secondItemType:secondItemType];
|
||||
return lookinConstraint;
|
||||
}
|
||||
return nil;
|
||||
}];
|
||||
return lookinConstraints.count ? lookinConstraints : nil;
|
||||
}
|
||||
|
||||
- (LookinConstraintItemType)_lks_constraintItemTypeForItem:(id)item {
|
||||
if (!item) {
|
||||
return LookinConstraintItemTypeNil;
|
||||
}
|
||||
if (item == self) {
|
||||
return LookinConstraintItemTypeSelf;
|
||||
}
|
||||
if (item == self.superview) {
|
||||
return LookinConstraintItemTypeSuper;
|
||||
}
|
||||
|
||||
// 在 runtime 时,这里会遇到的 UILayoutGuide 和 _UILayoutGuide 居然是 UIView 的子类,不知道是看错了还是有什么玄机,所以在判断是否是 UIView 之前要先判断这个
|
||||
if (@available(iOS 9.0, *)) {
|
||||
if ([item isKindOfClass:[UILayoutGuide class]]) {
|
||||
return LookinConstraintItemTypeLayoutGuide;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *className = NSStringFromClass([item class]);
|
||||
if ([className hasSuffix:@"_UILayoutGuide"]) {
|
||||
return LookinConstraintItemTypeLayoutGuide;
|
||||
}
|
||||
|
||||
if ([item isKindOfClass:[UIView class]]) {
|
||||
return LookinConstraintItemTypeView;
|
||||
}
|
||||
|
||||
NSAssert(NO, @"");
|
||||
return LookinConstraintItemTypeUnknown;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
19
Pods/LookinServer/Src/Main/Server/Category/UIViewController+LookinServer.h
generated
Normal file
19
Pods/LookinServer/Src/Main/Server/Category/UIViewController+LookinServer.h
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIViewController+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/4/22.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIViewController (LookinServer)
|
||||
|
||||
+ (UIViewController *)lks_visibleViewController;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
48
Pods/LookinServer/Src/Main/Server/Category/UIViewController+LookinServer.m
generated
Normal file
48
Pods/LookinServer/Src/Main/Server/Category/UIViewController+LookinServer.m
generated
Normal file
@@ -0,0 +1,48 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIViewController+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/4/22.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UIViewController+LookinServer.h"
|
||||
#import "UIView+LookinServer.h"
|
||||
#import <objc/runtime.h>
|
||||
#import "LKS_MultiplatformAdapter.h"
|
||||
|
||||
@implementation UIViewController (LookinServer)
|
||||
|
||||
+ (nullable UIViewController *)lks_visibleViewController {
|
||||
|
||||
UIViewController *rootViewController = [LKS_MultiplatformAdapter keyWindow].rootViewController;
|
||||
UIViewController *visibleViewController = [rootViewController lks_visibleViewControllerIfExist];
|
||||
return visibleViewController;
|
||||
}
|
||||
|
||||
- (UIViewController *)lks_visibleViewControllerIfExist {
|
||||
|
||||
if (self.presentedViewController) {
|
||||
return [self.presentedViewController lks_visibleViewControllerIfExist];
|
||||
}
|
||||
|
||||
if ([self isKindOfClass:[UINavigationController class]]) {
|
||||
return [((UINavigationController *)self).visibleViewController lks_visibleViewControllerIfExist];
|
||||
}
|
||||
|
||||
if ([self isKindOfClass:[UITabBarController class]]) {
|
||||
return [((UITabBarController *)self).selectedViewController lks_visibleViewControllerIfExist];
|
||||
}
|
||||
|
||||
if (self.isViewLoaded && !self.view.hidden && self.view.alpha > 0.01) {
|
||||
return self;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
21
Pods/LookinServer/Src/Main/Server/Category/UIVisualEffectView+LookinServer.h
generated
Normal file
21
Pods/LookinServer/Src/Main/Server/Category/UIVisualEffectView+LookinServer.h
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIVisualEffectView+LookinServer.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/10/8.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIVisualEffectView (LookinServer)
|
||||
|
||||
- (void)setLks_blurEffectStyleNumber:(NSNumber *)lks_blurEffectStyleNumber;
|
||||
|
||||
- (NSNumber *)lks_blurEffectStyleNumber;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
33
Pods/LookinServer/Src/Main/Server/Category/UIVisualEffectView+LookinServer.m
generated
Normal file
33
Pods/LookinServer/Src/Main/Server/Category/UIVisualEffectView+LookinServer.m
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// UIVisualEffectView+LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/10/8.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "UIVisualEffectView+LookinServer.h"
|
||||
#import "UIBlurEffect+LookinServer.h"
|
||||
|
||||
@implementation UIVisualEffectView (LookinServer)
|
||||
|
||||
- (void)setLks_blurEffectStyleNumber:(NSNumber *)lks_blurEffectStyleNumber {
|
||||
UIBlurEffectStyle style = [lks_blurEffectStyleNumber integerValue];
|
||||
UIBlurEffect *effect = [UIBlurEffect effectWithStyle:style];
|
||||
self.effect = effect;
|
||||
}
|
||||
|
||||
- (NSNumber *)lks_blurEffectStyleNumber {
|
||||
UIVisualEffect *effect = self.effect;
|
||||
if (![effect isKindOfClass:[UIBlurEffect class]]) {
|
||||
return nil;
|
||||
}
|
||||
UIBlurEffect *blurEffect = (UIBlurEffect *)effect;
|
||||
return blurEffect.lks_effectStyleNumber;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
29
Pods/LookinServer/Src/Main/Server/Connection/LKS_ConnectionManager.h
generated
Normal file
29
Pods/LookinServer/Src/Main/Server/Connection/LKS_ConnectionManager.h
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// Lookin.h
|
||||
// Lookin
|
||||
//
|
||||
// Created by Li Kai on 2018/8/5.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
extern NSString *const LKS_ConnectionDidEndNotificationName;
|
||||
|
||||
@class LookinConnectionResponseAttachment;
|
||||
|
||||
@interface LKS_ConnectionManager : NSObject
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
@property(nonatomic, assign) BOOL applicationIsActive;
|
||||
|
||||
- (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag;
|
||||
|
||||
- (void)pushData:(NSObject *)data type:(uint32_t)type;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
268
Pods/LookinServer/Src/Main/Server/Connection/LKS_ConnectionManager.m
generated
Normal file
268
Pods/LookinServer/Src/Main/Server/Connection/LKS_ConnectionManager.m
generated
Normal file
@@ -0,0 +1,268 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// LookinServer.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2018/8/5.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "LKS_ConnectionManager.h"
|
||||
#import "Lookin_PTChannel.h"
|
||||
#import "LKS_RequestHandler.h"
|
||||
#import "LookinConnectionResponseAttachment.h"
|
||||
#import "LKS_ExportManager.h"
|
||||
#import "LookinServerDefines.h"
|
||||
#import "LKS_TraceManager.h"
|
||||
#import "LKS_MultiplatformAdapter.h"
|
||||
|
||||
NSString *const LKS_ConnectionDidEndNotificationName = @"LKS_ConnectionDidEndNotificationName";
|
||||
|
||||
@interface LKS_ConnectionManager () <Lookin_PTChannelDelegate>
|
||||
|
||||
@property(nonatomic, weak) Lookin_PTChannel *peerChannel_;
|
||||
|
||||
@property(nonatomic, strong) LKS_RequestHandler *requestHandler;
|
||||
|
||||
@end
|
||||
|
||||
@implementation LKS_ConnectionManager
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static LKS_ConnectionManager *sharedInstance;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[LKS_ConnectionManager alloc] init];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
+ (void)load {
|
||||
// 触发 init 方法
|
||||
[LKS_ConnectionManager sharedInstance];
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
NSLog(@"LookinServer - Will launch. Framework version: %@", LOOKIN_SERVER_READABLE_VERSION);
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleApplicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleWillResignActiveNotification) name:UIApplicationWillResignActiveNotification object:nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleLocalInspect:) name:@"Lookin_2D" object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleLocalInspect:) name:@"Lookin_3D" object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:@"Lookin_Export" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
|
||||
[[LKS_ExportManager sharedInstance] exportAndShare];
|
||||
}];
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:@"Lookin_RelationSearch" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
|
||||
[[LKS_TraceManager sharedInstance] addSearchTarger:note.object];
|
||||
}];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleGetLookinInfo:) name:@"GetLookinInfo" object:nil];
|
||||
|
||||
self.requestHandler = [LKS_RequestHandler new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_handleWillResignActiveNotification {
|
||||
self.applicationIsActive = NO;
|
||||
|
||||
if (self.peerChannel_ && ![self.peerChannel_ isConnected]) {
|
||||
[self.peerChannel_ close];
|
||||
self.peerChannel_ = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_handleApplicationDidBecomeActive {
|
||||
self.applicationIsActive = YES;
|
||||
[self searchPortToListenIfNoConnection];
|
||||
}
|
||||
|
||||
- (void)searchPortToListenIfNoConnection {
|
||||
if ([self.peerChannel_ isConnected]) {
|
||||
NSLog(@"LookinServer - Abort to search ports. Already has connected channel.");
|
||||
return;
|
||||
}
|
||||
NSLog(@"LookinServer - Searching port to listen...");
|
||||
[self.peerChannel_ close];
|
||||
self.peerChannel_ = nil;
|
||||
|
||||
if ([self isiOSAppOnMac]) {
|
||||
[self _tryToListenOnPortFrom:LookinSimulatorIPv4PortNumberStart to:LookinSimulatorIPv4PortNumberEnd current:LookinSimulatorIPv4PortNumberStart];
|
||||
} else {
|
||||
[self _tryToListenOnPortFrom:LookinUSBDeviceIPv4PortNumberStart to:LookinUSBDeviceIPv4PortNumberEnd current:LookinUSBDeviceIPv4PortNumberStart];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isiOSAppOnMac {
|
||||
#if TARGET_OS_SIMULATOR
|
||||
return YES;
|
||||
#else
|
||||
if (@available(iOS 14.0, *)) {
|
||||
// isiOSAppOnMac 这个 API 看似在 iOS 14.0 上可用,但其实在 iOS 14 beta 上是不存在的、有 unrecognized selector 问题,因此这里要用 respondsToSelector 做一下保护
|
||||
NSProcessInfo *info = [NSProcessInfo processInfo];
|
||||
if ([info respondsToSelector:@selector(isiOSAppOnMac)]) {
|
||||
return [info isiOSAppOnMac];
|
||||
} else if ([info respondsToSelector:@selector(isMacCatalystApp)]) {
|
||||
return [info isMacCatalystApp];
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||||
return [NSProcessInfo processInfo].isMacCatalystApp;
|
||||
}
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)_tryToListenOnPortFrom:(int)fromPort to:(int)toPort current:(int)currentPort {
|
||||
Lookin_PTChannel *channel = [Lookin_PTChannel channelWithDelegate:self];
|
||||
channel.targetPort = currentPort;
|
||||
[channel listenOnPort:currentPort IPv4Address:INADDR_LOOPBACK callback:^(NSError *error) {
|
||||
if (error) {
|
||||
if (error.code == 48) {
|
||||
// 该地址已被占用
|
||||
} else {
|
||||
// 未知失败
|
||||
}
|
||||
|
||||
if (currentPort < toPort) {
|
||||
// 尝试下一个端口
|
||||
NSLog(@"LookinServer - 127.0.0.1:%d is unavailable(%@). Will try anothor address ...", currentPort, error);
|
||||
[self _tryToListenOnPortFrom:fromPort to:toPort current:(currentPort + 1)];
|
||||
} else {
|
||||
// 所有端口都尝试完毕,全部失败
|
||||
NSLog(@"LookinServer - 127.0.0.1:%d is unavailable(%@).", currentPort, error);
|
||||
NSLog(@"LookinServer - Connect failed in the end.");
|
||||
}
|
||||
|
||||
} else {
|
||||
// 成功
|
||||
NSLog(@"LookinServer - Connected successfully on 127.0.0.1:%d", currentPort);
|
||||
// 此时 peerChannel_ 状态为 listening
|
||||
self.peerChannel_ = channel;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (self.peerChannel_) {
|
||||
[self.peerChannel_ close];
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)respond:(LookinConnectionResponseAttachment *)data requestType:(uint32_t)requestType tag:(uint32_t)tag {
|
||||
[self _sendData:data frameOfType:requestType tag:tag];
|
||||
}
|
||||
|
||||
- (void)pushData:(NSObject *)data type:(uint32_t)type {
|
||||
[self _sendData:data frameOfType:type tag:0];
|
||||
}
|
||||
|
||||
- (void)_sendData:(NSObject *)data frameOfType:(uint32_t)frameOfType tag:(uint32_t)tag {
|
||||
if (self.peerChannel_) {
|
||||
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:data];
|
||||
dispatch_data_t payload = [archivedData createReferencingDispatchData];
|
||||
|
||||
[self.peerChannel_ sendFrameOfType:frameOfType tag:tag withPayload:payload callback:^(NSError *error) {
|
||||
if (error) {
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lookin_PTChannelDelegate
|
||||
|
||||
- (BOOL)ioFrameChannel:(Lookin_PTChannel*)channel shouldAcceptFrameOfType:(uint32_t)type tag:(uint32_t)tag payloadSize:(uint32_t)payloadSize {
|
||||
if (channel != self.peerChannel_) {
|
||||
return NO;
|
||||
} else if ([self.requestHandler canHandleRequestType:type]) {
|
||||
return YES;
|
||||
} else {
|
||||
[channel close];
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)ioFrameChannel:(Lookin_PTChannel*)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(Lookin_PTData*)payload {
|
||||
id object = nil;
|
||||
if (payload) {
|
||||
id unarchivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:[NSData dataWithContentsOfDispatchData:payload.dispatchData]];
|
||||
if ([unarchivedObject isKindOfClass:[LookinConnectionAttachment class]]) {
|
||||
LookinConnectionAttachment *attachment = (LookinConnectionAttachment *)unarchivedObject;
|
||||
object = attachment.data;
|
||||
} else {
|
||||
object = unarchivedObject;
|
||||
}
|
||||
}
|
||||
[self.requestHandler handleRequestType:type tag:tag object:object];
|
||||
}
|
||||
|
||||
/// 当 Client 端链接成功时,该方法会被调用,然后 channel 的状态会变成 connected
|
||||
- (void)ioFrameChannel:(Lookin_PTChannel*)channel didAcceptConnection:(Lookin_PTChannel*)otherChannel fromAddress:(Lookin_PTAddress*)address {
|
||||
NSLog(@"LookinServer - channel:%@, acceptConnection:%@", channel.debugTag, otherChannel.debugTag);
|
||||
|
||||
Lookin_PTChannel *previousChannel = self.peerChannel_;
|
||||
|
||||
otherChannel.targetPort = address.port;
|
||||
self.peerChannel_ = otherChannel;
|
||||
|
||||
[previousChannel cancel];
|
||||
}
|
||||
|
||||
/// 当连接过 Lookin 客户端,然后 Lookin 客户端又被关闭时,会走到这里
|
||||
- (void)ioFrameChannel:(Lookin_PTChannel*)channel didEndWithError:(NSError*)error {
|
||||
if (self.peerChannel_ != channel) {
|
||||
// Client 端第一次连接上时,之前 listen 的 port 会被 Peertalk 内部 cancel(并在 didAcceptConnection 方法里给业务抛一个新建的 connected 状态的 channel),那个被 cancel 的 channel 会走到这里
|
||||
NSLog(@"LookinServer - Ignore channel%@ end.", channel.debugTag);
|
||||
return;
|
||||
}
|
||||
// Client 端关闭时,会走到这里
|
||||
NSLog(@"LookinServer - channel%@ DidEndWithError:%@", channel.debugTag, error);
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:LKS_ConnectionDidEndNotificationName object:self];
|
||||
[self searchPortToListenIfNoConnection];
|
||||
}
|
||||
|
||||
#pragma mark - Handler
|
||||
|
||||
- (void)_handleLocalInspect:(NSNotification *)note {
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Lookin" message:@"Failed to run local inspection. The feature has been removed. Please use the computer version of Lookin or consider SDKs like FLEX for similar functionality." preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
|
||||
[alertController addAction:okAction];
|
||||
UIWindow *keyWindow = [LKS_MultiplatformAdapter keyWindow];
|
||||
UIViewController *rootViewController = [keyWindow rootViewController];
|
||||
[rootViewController presentViewController:alertController animated:YES completion:nil];
|
||||
|
||||
NSLog(@"LookinServer - Failed to run local inspection. The feature has been removed. Please use the computer version of Lookin or consider SDKs like FLEX for similar functionality.");
|
||||
}
|
||||
|
||||
- (void)handleGetLookinInfo:(NSNotification *)note {
|
||||
NSDictionary* userInfo = note.userInfo;
|
||||
if (!userInfo) {
|
||||
return;
|
||||
}
|
||||
NSMutableDictionary* infoWrapper = userInfo[@"infos"];
|
||||
if (![infoWrapper isKindOfClass:[NSMutableDictionary class]]) {
|
||||
NSLog(@"LookinServer - GetLookinInfo failed. Params invalid.");
|
||||
return;
|
||||
}
|
||||
infoWrapper[@"lookinServerVersion"] = LOOKIN_SERVER_READABLE_VERSION;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/// 这个类使得用户可以通过 NSClassFromString(@"Lookin") 来判断 LookinServer 是否被编译进了项目里
|
||||
|
||||
@interface Lookin : NSObject
|
||||
|
||||
@end
|
||||
|
||||
@implementation Lookin
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
21
Pods/LookinServer/Src/Main/Server/Connection/LKS_RequestHandler.h
generated
Normal file
21
Pods/LookinServer/Src/Main/Server/Connection/LKS_RequestHandler.h
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// LKS_RequestHandler.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/1/15.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface LKS_RequestHandler : NSObject
|
||||
|
||||
- (BOOL)canHandleRequestType:(uint32_t)requestType;
|
||||
|
||||
- (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)object;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
558
Pods/LookinServer/Src/Main/Server/Connection/LKS_RequestHandler.m
generated
Normal file
558
Pods/LookinServer/Src/Main/Server/Connection/LKS_RequestHandler.m
generated
Normal file
@@ -0,0 +1,558 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// LKS_RequestHandler.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/1/15.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "LKS_RequestHandler.h"
|
||||
#import "NSObject+LookinServer.h"
|
||||
#import "UIImage+LookinServer.h"
|
||||
#import "LKS_ConnectionManager.h"
|
||||
#import "LookinConnectionResponseAttachment.h"
|
||||
#import "LookinAttributeModification.h"
|
||||
#import "LookinDisplayItemDetail.h"
|
||||
#import "LookinHierarchyInfo.h"
|
||||
#import "LookinServerDefines.h"
|
||||
#import <objc/runtime.h>
|
||||
#import "LookinObject.h"
|
||||
#import "LookinAppInfo.h"
|
||||
#import "LKS_AttrGroupsMaker.h"
|
||||
#import "LKS_InbuiltAttrModificationHandler.h"
|
||||
#import "LKS_CustomAttrModificationHandler.h"
|
||||
#import "LKS_AttrModificationPatchHandler.h"
|
||||
#import "LKS_HierarchyDetailsHandler.h"
|
||||
#import "LookinStaticAsyncUpdateTask.h"
|
||||
|
||||
@interface LKS_RequestHandler ()
|
||||
|
||||
@property(nonatomic, strong) NSMutableSet<LKS_HierarchyDetailsHandler *> *activeDetailHandlers;
|
||||
|
||||
@end
|
||||
|
||||
@implementation LKS_RequestHandler {
|
||||
NSSet *_validRequestTypes;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_validRequestTypes = [NSSet setWithObjects:@(LookinRequestTypePing),
|
||||
@(LookinRequestTypeApp),
|
||||
@(LookinRequestTypeHierarchy),
|
||||
@(LookinRequestTypeInbuiltAttrModification),
|
||||
@(LookinRequestTypeCustomAttrModification),
|
||||
@(LookinRequestTypeAttrModificationPatch),
|
||||
@(LookinRequestTypeHierarchyDetails),
|
||||
@(LookinRequestTypeFetchObject),
|
||||
@(LookinRequestTypeAllAttrGroups),
|
||||
@(LookinRequestTypeAllSelectorNames),
|
||||
@(LookinRequestTypeInvokeMethod),
|
||||
@(LookinRequestTypeFetchImageViewImage),
|
||||
@(LookinRequestTypeModifyRecognizerEnable),
|
||||
@(LookinPush_CanceHierarchyDetails),
|
||||
nil];
|
||||
|
||||
self.activeDetailHandlers = [NSMutableSet set];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)canHandleRequestType:(uint32_t)requestType {
|
||||
if ([_validRequestTypes containsObject:@(requestType)]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)handleRequestType:(uint32_t)requestType tag:(uint32_t)tag object:(id)object {
|
||||
if (requestType == LookinRequestTypePing) {
|
||||
LookinConnectionResponseAttachment *responseAttachment = [LookinConnectionResponseAttachment new];
|
||||
// 当 app 处于后台时,可能可以执行代码也可能不能执行代码,如果运气好了可以执行代码,则这里直接主动使用 appIsInBackground 标识 app 处于后台,不要让 Lookin 客户端傻傻地等待超时了
|
||||
if (![LKS_ConnectionManager sharedInstance].applicationIsActive) {
|
||||
responseAttachment.appIsInBackground = YES;
|
||||
}
|
||||
[[LKS_ConnectionManager sharedInstance] respond:responseAttachment requestType:requestType tag:tag];
|
||||
|
||||
} else if (requestType == LookinRequestTypeApp) {
|
||||
// 请求可用设备信息
|
||||
if (![object isKindOfClass:[NSDictionary class]]) {
|
||||
[self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
NSDictionary<NSString *, id> *params = object;
|
||||
BOOL needImages = ((NSNumber *)params[@"needImages"]).boolValue;
|
||||
NSArray<NSNumber *> *localIdentifiers = params[@"local"];
|
||||
|
||||
LookinAppInfo *appInfo = [LookinAppInfo currentInfoWithScreenshot:needImages icon:needImages localIdentifiers:localIdentifiers];
|
||||
|
||||
LookinConnectionResponseAttachment *responseAttachment = [LookinConnectionResponseAttachment new];
|
||||
responseAttachment.data = appInfo;
|
||||
[[LKS_ConnectionManager sharedInstance] respond:responseAttachment requestType:requestType tag:tag];
|
||||
|
||||
} else if (requestType == LookinRequestTypeHierarchy) {
|
||||
// 从 LookinClient 1.0.4 开始有这个参数,之前是 nil
|
||||
NSString *clientVersion = nil;
|
||||
if ([object isKindOfClass:[NSDictionary class]]) {
|
||||
NSDictionary<NSString *, id> *params = object;
|
||||
NSString *version = params[@"clientVersion"];
|
||||
if ([version isKindOfClass:[NSString class]]) {
|
||||
clientVersion = version;
|
||||
}
|
||||
}
|
||||
|
||||
LookinConnectionResponseAttachment *responseAttachment = [LookinConnectionResponseAttachment new];
|
||||
responseAttachment.data = [LookinHierarchyInfo staticInfoWithLookinVersion:clientVersion];
|
||||
[[LKS_ConnectionManager sharedInstance] respond:responseAttachment requestType:requestType tag:tag];
|
||||
|
||||
} else if (requestType == LookinRequestTypeInbuiltAttrModification) {
|
||||
// 请求修改某个属性
|
||||
[LKS_InbuiltAttrModificationHandler handleModification:object completion:^(LookinDisplayItemDetail *data, NSError *error) {
|
||||
LookinConnectionResponseAttachment *attachment = [LookinConnectionResponseAttachment new];
|
||||
if (error) {
|
||||
attachment.error = error;
|
||||
} else {
|
||||
attachment.data = data;
|
||||
}
|
||||
[[LKS_ConnectionManager sharedInstance] respond:attachment requestType:requestType tag:tag];
|
||||
}];
|
||||
|
||||
} else if (requestType == LookinRequestTypeCustomAttrModification) {
|
||||
BOOL succ = [LKS_CustomAttrModificationHandler handleModification:object];
|
||||
if (succ) {
|
||||
[self _submitResponseWithData:nil requestType:requestType tag:tag];
|
||||
} else {
|
||||
[self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag];
|
||||
}
|
||||
|
||||
} else if (requestType == LookinRequestTypeAttrModificationPatch) {
|
||||
NSArray<LookinStaticAsyncUpdateTask *> *tasks = object;
|
||||
NSUInteger dataTotalCount = tasks.count;
|
||||
[LKS_InbuiltAttrModificationHandler handlePatchWithTasks:tasks block:^(LookinDisplayItemDetail *data) {
|
||||
LookinConnectionResponseAttachment *attrAttachment = [LookinConnectionResponseAttachment new];
|
||||
attrAttachment.data = data;
|
||||
attrAttachment.dataTotalCount = dataTotalCount;
|
||||
attrAttachment.currentDataCount = 1;
|
||||
[[LKS_ConnectionManager sharedInstance] respond:attrAttachment requestType:LookinRequestTypeAttrModificationPatch tag:tag];
|
||||
}];
|
||||
|
||||
} else if (requestType == LookinRequestTypeHierarchyDetails) {
|
||||
NSArray<LookinStaticAsyncUpdateTasksPackage *> *packages = object;
|
||||
NSUInteger responsesDataTotalCount = [packages lookin_reduceInteger:^NSInteger(NSInteger accumulator, NSUInteger idx, LookinStaticAsyncUpdateTasksPackage *package) {
|
||||
accumulator += package.tasks.count;
|
||||
return accumulator;
|
||||
} initialAccumlator:0];
|
||||
|
||||
LKS_HierarchyDetailsHandler *handler = [LKS_HierarchyDetailsHandler new];
|
||||
[self.activeDetailHandlers addObject:handler];
|
||||
|
||||
[handler startWithPackages:packages block:^(NSArray<LookinDisplayItemDetail *> *details) {
|
||||
LookinConnectionResponseAttachment *attachment = [LookinConnectionResponseAttachment new];
|
||||
attachment.data = details;
|
||||
attachment.dataTotalCount = responsesDataTotalCount;
|
||||
attachment.currentDataCount = details.count;
|
||||
[[LKS_ConnectionManager sharedInstance] respond:attachment requestType:LookinRequestTypeHierarchyDetails tag:tag];
|
||||
|
||||
} finishedBlock:^{
|
||||
[self.activeDetailHandlers removeObject:handler];
|
||||
}];
|
||||
|
||||
} else if (requestType == LookinRequestTypeFetchObject) {
|
||||
unsigned long oid = ((NSNumber *)object).unsignedLongValue;
|
||||
NSObject *object = [NSObject lks_objectWithOid:oid];
|
||||
LookinObject *lookinObj = [LookinObject instanceWithObject:object];
|
||||
|
||||
LookinConnectionResponseAttachment *attach = [LookinConnectionResponseAttachment new];
|
||||
attach.data = lookinObj;
|
||||
[[LKS_ConnectionManager sharedInstance] respond:attach requestType:requestType tag:tag];
|
||||
|
||||
} else if (requestType == LookinRequestTypeAllAttrGroups) {
|
||||
unsigned long oid = ((NSNumber *)object).unsignedLongValue;
|
||||
CALayer *layer = (CALayer *)[NSObject lks_objectWithOid:oid];
|
||||
if (![layer isKindOfClass:[CALayer class]]) {
|
||||
[self _submitResponseWithError:LookinErr_ObjNotFound requestType:LookinRequestTypeAllAttrGroups tag:tag];
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<LookinAttributesGroup *> *list = [LKS_AttrGroupsMaker attrGroupsForLayer:layer];
|
||||
[self _submitResponseWithData:list requestType:LookinRequestTypeAllAttrGroups tag:tag];
|
||||
|
||||
} else if (requestType == LookinRequestTypeAllSelectorNames) {
|
||||
if (![object isKindOfClass:[NSDictionary class]]) {
|
||||
[self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
NSDictionary *params = object;
|
||||
Class targetClass = NSClassFromString(params[@"className"]);
|
||||
BOOL hasArg = [(NSNumber *)params[@"hasArg"] boolValue];
|
||||
if (!targetClass) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:LKS_Localized(@"Didn't find the class named \"%@\". Please input another class and try again."), object];
|
||||
[self _submitResponseWithError:LookinErrorMake(errorMsg, @"") requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<NSString *> *selNames = [self _methodNameListForClass:targetClass hasArg:hasArg];
|
||||
[self _submitResponseWithData:selNames requestType:requestType tag:tag];
|
||||
|
||||
} else if (requestType == LookinRequestTypeInvokeMethod) {
|
||||
if (![object isKindOfClass:[NSDictionary class]]) {
|
||||
[self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
NSDictionary *param = object;
|
||||
unsigned long oid = [param[@"oid"] unsignedLongValue];
|
||||
NSString *text = param[@"text"];
|
||||
if (!text.length) {
|
||||
[self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
NSObject *targerObj = [NSObject lks_objectWithOid:oid];
|
||||
if (!targerObj) {
|
||||
[self _submitResponseWithError:LookinErr_ObjNotFound requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
|
||||
SEL targetSelector = NSSelectorFromString(text);
|
||||
if (targetSelector && [targerObj respondsToSelector:targetSelector]) {
|
||||
NSString *resultDescription;
|
||||
NSObject *resultObject;
|
||||
NSError *error;
|
||||
[self _handleInvokeWithObject:targerObj selector:targetSelector resultDescription:&resultDescription resultObject:&resultObject error:&error];
|
||||
if (error) {
|
||||
[self _submitResponseWithError:error requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
NSMutableDictionary *responseData = [NSMutableDictionary dictionaryWithCapacity:2];
|
||||
if (resultDescription) {
|
||||
responseData[@"description"] = resultDescription;
|
||||
}
|
||||
if (resultObject) {
|
||||
responseData[@"object"] = resultObject;
|
||||
}
|
||||
[self _submitResponseWithData:responseData requestType:requestType tag:tag];
|
||||
} else {
|
||||
NSString *errMsg = [NSString stringWithFormat:LKS_Localized(@"%@ doesn't have an instance method called \"%@\"."), NSStringFromClass(targerObj.class), text];
|
||||
[self _submitResponseWithError:LookinErrorMake(errMsg, @"") requestType:requestType tag:tag];
|
||||
}
|
||||
|
||||
} else if (requestType == LookinPush_CanceHierarchyDetails) {
|
||||
[self.activeDetailHandlers enumerateObjectsUsingBlock:^(LKS_HierarchyDetailsHandler * _Nonnull handler, BOOL * _Nonnull stop) {
|
||||
[handler cancel];
|
||||
}];
|
||||
[self.activeDetailHandlers removeAllObjects];
|
||||
|
||||
} else if (requestType == LookinRequestTypeFetchImageViewImage) {
|
||||
if (![object isKindOfClass:[NSNumber class]]) {
|
||||
[self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
unsigned long imageViewOid = [(NSNumber *)object unsignedLongValue];
|
||||
UIImageView *imageView = (UIImageView *)[NSObject lks_objectWithOid:imageViewOid];
|
||||
if (!imageView) {
|
||||
[self _submitResponseWithError:LookinErr_ObjNotFound requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
if (![imageView isKindOfClass:[UIImageView class]]) {
|
||||
[self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
UIImage *image = imageView.image;
|
||||
NSData *imageData = [image lookin_data];
|
||||
[self _submitResponseWithData:imageData requestType:requestType tag:tag];
|
||||
|
||||
} else if (requestType == LookinRequestTypeModifyRecognizerEnable) {
|
||||
if (![object isKindOfClass:[NSDictionary class]]) {
|
||||
[self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
NSDictionary<NSString *, NSNumber *> *params = object;
|
||||
unsigned long recognizerOid = ((NSNumber *)params[@"oid"]).unsignedLongValue;
|
||||
BOOL shouldBeEnabled = ((NSNumber *)params[@"enable"]).boolValue;
|
||||
|
||||
UIGestureRecognizer *recognizer = (UIGestureRecognizer *)[NSObject lks_objectWithOid:recognizerOid];
|
||||
if (!recognizer) {
|
||||
[self _submitResponseWithError:LookinErr_ObjNotFound requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
if (![recognizer isKindOfClass:[UIGestureRecognizer class]]) {
|
||||
[self _submitResponseWithError:LookinErr_Inner requestType:requestType tag:tag];
|
||||
return;
|
||||
}
|
||||
recognizer.enabled = shouldBeEnabled;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
// dispatch 以确保拿到的 enabled 是比较新的
|
||||
[self _submitResponseWithData:@(recognizer.enabled) requestType:requestType tag:tag];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)_methodNameListForClass:(Class)aClass hasArg:(BOOL)hasArg {
|
||||
NSSet<NSString *> *prefixesToVoid = [NSSet setWithObjects:@"_", @"CA_", @"cpl", @"mf_", @"vs_", @"pep_", @"isNS", @"avkit_", @"PG_", @"px_", @"pl_", @"nsli_", @"pu_", @"pxg_", nil];
|
||||
NSMutableArray<NSString *> *array = [NSMutableArray array];
|
||||
|
||||
Class currentClass = aClass;
|
||||
while (currentClass) {
|
||||
NSString *className = NSStringFromClass(currentClass);
|
||||
BOOL isSystemClass = ([className hasPrefix:@"UI"] || [className hasPrefix:@"CA"] || [className hasPrefix:@"NS"]);
|
||||
|
||||
unsigned int methodCount = 0;
|
||||
Method *methods = class_copyMethodList(currentClass, &methodCount);
|
||||
for (unsigned int i = 0; i < methodCount; i++) {
|
||||
NSString *selName = NSStringFromSelector(method_getName(methods[i]));
|
||||
|
||||
if (!hasArg && [selName containsString:@":"]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSystemClass) {
|
||||
BOOL invalid = [prefixesToVoid lookin_any:^BOOL(NSString *prefix) {
|
||||
return [selName hasPrefix:prefix];
|
||||
}];
|
||||
if (invalid) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (selName.length && ![array containsObject:selName]) {
|
||||
[array addObject:selName];
|
||||
}
|
||||
}
|
||||
if (methods) free(methods);
|
||||
currentClass = [currentClass superclass];
|
||||
}
|
||||
|
||||
return [array lookin_sortedArrayByStringLength];
|
||||
}
|
||||
|
||||
- (void)_handleInvokeWithObject:(NSObject *)obj selector:(SEL)selector resultDescription:(NSString **)description resultObject:(LookinObject **)resultObject error:(NSError **)error {
|
||||
NSMethodSignature *signature = [obj methodSignatureForSelector:selector];
|
||||
if (signature.numberOfArguments > 2) {
|
||||
*error = LookinErrorMake(LKS_Localized(@"Lookin doesn't support invoking methods with arguments yet."), @"");
|
||||
return;
|
||||
}
|
||||
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
|
||||
[invocation setTarget:obj];
|
||||
[invocation setSelector:selector];
|
||||
[invocation invoke];
|
||||
|
||||
const char *returnType = [signature methodReturnType];
|
||||
|
||||
|
||||
if (strcmp(returnType, @encode(void)) == 0) {
|
||||
//void, do nothing
|
||||
*description = LookinStringFlag_VoidReturn;
|
||||
|
||||
} else if (strcmp(returnType, @encode(char)) == 0) {
|
||||
char charValue;
|
||||
[invocation getReturnValue:&charValue];
|
||||
*description = [NSString stringWithFormat:@"%@", @(charValue)];
|
||||
|
||||
} else if (strcmp(returnType, @encode(int)) == 0) {
|
||||
int intValue;
|
||||
[invocation getReturnValue:&intValue];
|
||||
if (intValue == INT_MAX) {
|
||||
*description = @"INT_MAX";
|
||||
} else if (intValue == INT_MIN) {
|
||||
*description = @"INT_MIN";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(intValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(short)) == 0) {
|
||||
short shortValue;
|
||||
[invocation getReturnValue:&shortValue];
|
||||
if (shortValue == SHRT_MAX) {
|
||||
*description = @"SHRT_MAX";
|
||||
} else if (shortValue == SHRT_MIN) {
|
||||
*description = @"SHRT_MIN";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(shortValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(long)) == 0) {
|
||||
long longValue;
|
||||
[invocation getReturnValue:&longValue];
|
||||
if (longValue == NSNotFound) {
|
||||
*description = @"NSNotFound";
|
||||
} else if (longValue == LONG_MAX) {
|
||||
*description = @"LONG_MAX";
|
||||
} else if (longValue == LONG_MIN) {
|
||||
*description = @"LONG_MAX";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(longValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(long long)) == 0) {
|
||||
long long longLongValue;
|
||||
[invocation getReturnValue:&longLongValue];
|
||||
if (longLongValue == LLONG_MAX) {
|
||||
*description = @"LLONG_MAX";
|
||||
} else if (longLongValue == LLONG_MIN) {
|
||||
*description = @"LLONG_MIN";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(longLongValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(unsigned char)) == 0) {
|
||||
unsigned char ucharValue;
|
||||
[invocation getReturnValue:&ucharValue];
|
||||
if (ucharValue == UCHAR_MAX) {
|
||||
*description = @"UCHAR_MAX";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(ucharValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(unsigned int)) == 0) {
|
||||
unsigned int uintValue;
|
||||
[invocation getReturnValue:&uintValue];
|
||||
if (uintValue == UINT_MAX) {
|
||||
*description = @"UINT_MAX";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(uintValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(unsigned short)) == 0) {
|
||||
unsigned short ushortValue;
|
||||
[invocation getReturnValue:&ushortValue];
|
||||
if (ushortValue == USHRT_MAX) {
|
||||
*description = @"USHRT_MAX";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(ushortValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(unsigned long)) == 0) {
|
||||
unsigned long ulongValue;
|
||||
[invocation getReturnValue:&ulongValue];
|
||||
if (ulongValue == ULONG_MAX) {
|
||||
*description = @"ULONG_MAX";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(ulongValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(unsigned long long)) == 0) {
|
||||
unsigned long long ulongLongValue;
|
||||
[invocation getReturnValue:&ulongLongValue];
|
||||
if (ulongLongValue == ULONG_LONG_MAX) {
|
||||
*description = @"ULONG_LONG_MAX";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(ulongLongValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(float)) == 0) {
|
||||
float floatValue;
|
||||
[invocation getReturnValue:&floatValue];
|
||||
if (floatValue == FLT_MAX) {
|
||||
*description = @"FLT_MAX";
|
||||
} else if (floatValue == FLT_MIN) {
|
||||
*description = @"FLT_MIN";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(floatValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(double)) == 0) {
|
||||
double doubleValue;
|
||||
[invocation getReturnValue:&doubleValue];
|
||||
if (doubleValue == DBL_MAX) {
|
||||
*description = @"DBL_MAX";
|
||||
} else if (doubleValue == DBL_MIN) {
|
||||
*description = @"DBL_MIN";
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:@"%@", @(doubleValue)];
|
||||
}
|
||||
|
||||
} else if (strcmp(returnType, @encode(BOOL)) == 0) {
|
||||
BOOL boolValue;
|
||||
[invocation getReturnValue:&boolValue];
|
||||
*description = boolValue ? @"YES" : @"NO";
|
||||
|
||||
} else if (strcmp(returnType, @encode(SEL)) == 0) {
|
||||
SEL selValue;
|
||||
[invocation getReturnValue:&selValue];
|
||||
*description = [NSString stringWithFormat:@"SEL(%@)", NSStringFromSelector(selValue)];
|
||||
|
||||
} else if (strcmp(returnType, @encode(Class)) == 0) {
|
||||
Class classValue;
|
||||
[invocation getReturnValue:&classValue];
|
||||
*description = [NSString stringWithFormat:@"<%@>", NSStringFromClass(classValue)];
|
||||
|
||||
} else if (strcmp(returnType, @encode(CGPoint)) == 0) {
|
||||
CGPoint targetValue;
|
||||
[invocation getReturnValue:&targetValue];
|
||||
*description = NSStringFromCGPoint(targetValue);
|
||||
|
||||
} else if (strcmp(returnType, @encode(CGVector)) == 0) {
|
||||
CGVector targetValue;
|
||||
[invocation getReturnValue:&targetValue];
|
||||
*description = NSStringFromCGVector(targetValue);
|
||||
|
||||
} else if (strcmp(returnType, @encode(CGSize)) == 0) {
|
||||
CGSize targetValue;
|
||||
[invocation getReturnValue:&targetValue];
|
||||
*description = NSStringFromCGSize(targetValue);
|
||||
|
||||
} else if (strcmp(returnType, @encode(CGRect)) == 0) {
|
||||
CGRect rectValue;
|
||||
[invocation getReturnValue:&rectValue];
|
||||
*description = NSStringFromCGRect(rectValue);
|
||||
|
||||
} else if (strcmp(returnType, @encode(CGAffineTransform)) == 0) {
|
||||
CGAffineTransform rectValue;
|
||||
[invocation getReturnValue:&rectValue];
|
||||
*description = NSStringFromCGAffineTransform(rectValue);
|
||||
|
||||
} else if (strcmp(returnType, @encode(UIEdgeInsets)) == 0) {
|
||||
UIEdgeInsets targetValue;
|
||||
[invocation getReturnValue:&targetValue];
|
||||
*description = NSStringFromUIEdgeInsets(targetValue);
|
||||
|
||||
} else if (strcmp(returnType, @encode(UIOffset)) == 0) {
|
||||
UIOffset targetValue;
|
||||
[invocation getReturnValue:&targetValue];
|
||||
*description = NSStringFromUIOffset(targetValue);
|
||||
|
||||
} else {
|
||||
if (@available(iOS 11.0, tvOS 11.0, *)) {
|
||||
if (strcmp(returnType, @encode(NSDirectionalEdgeInsets)) == 0) {
|
||||
NSDirectionalEdgeInsets targetValue;
|
||||
[invocation getReturnValue:&targetValue];
|
||||
*description = NSStringFromDirectionalEdgeInsets(targetValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *argType_string = [[NSString alloc] lookin_safeInitWithUTF8String:returnType];
|
||||
if ([argType_string hasPrefix:@"@"] || [argType_string hasPrefix:@"^{"]) {
|
||||
__unsafe_unretained id returnObjValue;
|
||||
[invocation getReturnValue:&returnObjValue];
|
||||
|
||||
if (returnObjValue) {
|
||||
*description = [NSString stringWithFormat:@"%@", returnObjValue];
|
||||
|
||||
LookinObject *parsedLookinObj = [LookinObject instanceWithObject:returnObjValue];
|
||||
*resultObject = parsedLookinObj;
|
||||
} else {
|
||||
*description = @"nil";
|
||||
}
|
||||
} else {
|
||||
*description = [NSString stringWithFormat:LKS_Localized(@"%@ was invoked successfully, but Lookin can't parse the return value:%@"), NSStringFromSelector(selector), argType_string];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_submitResponseWithError:(NSError *)error requestType:(uint32_t)requestType tag:(uint32_t)tag {
|
||||
LookinConnectionResponseAttachment *attachment = [LookinConnectionResponseAttachment new];
|
||||
attachment.error = error;
|
||||
[[LKS_ConnectionManager sharedInstance] respond:attachment requestType:requestType tag:tag];
|
||||
}
|
||||
|
||||
- (void)_submitResponseWithData:(NSObject *)data requestType:(uint32_t)requestType tag:(uint32_t)tag {
|
||||
LookinConnectionResponseAttachment *attachment = [LookinConnectionResponseAttachment new];
|
||||
attachment.data = data;
|
||||
[[LKS_ConnectionManager sharedInstance] respond:attachment requestType:requestType tag:tag];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
26
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_AttrModificationPatchHandler.h
generated
Normal file
26
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_AttrModificationPatchHandler.h
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// LKS_AttrModificationPatchHandler.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/6/12.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class LookinDisplayItemDetail;
|
||||
|
||||
@interface LKS_AttrModificationPatchHandler : NSObject
|
||||
|
||||
/**
|
||||
@param oids 数组内 idx 较小的应该为 displayItems 里的 subItem,idx 较大的应该为 superItem
|
||||
@param lowImageQuality 是否采用低图像质量
|
||||
@param block 该 block 会被多次调用,其中 tasksTotalCount 是总的调用次数(即可被用来作为 TotalResponseCount)
|
||||
*/
|
||||
+ (void)handleLayerOids:(NSArray<NSNumber *> *)oids lowImageQuality:(BOOL)lowImageQuality block:(void (^)(LookinDisplayItemDetail *detail, NSUInteger tasksTotalCount, NSError *error))block;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
51
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_AttrModificationPatchHandler.m
generated
Normal file
51
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_AttrModificationPatchHandler.m
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// LKS_AttrModificationPatchHandler.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/6/12.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import "LKS_AttrModificationPatchHandler.h"
|
||||
#import "LookinDisplayItemDetail.h"
|
||||
#import "LookinServerDefines.h"
|
||||
|
||||
@implementation LKS_AttrModificationPatchHandler
|
||||
|
||||
+ (void)handleLayerOids:(NSArray<NSNumber *> *)oids lowImageQuality:(BOOL)lowImageQuality block:(void (^)(LookinDisplayItemDetail *detail, NSUInteger tasksTotalCount, NSError *error))block {
|
||||
if (!block) {
|
||||
NSAssert(NO, @"");
|
||||
return;
|
||||
}
|
||||
if (![oids isKindOfClass:[NSArray class]]) {
|
||||
block(nil, 1, LookinErr_Inner);
|
||||
return;
|
||||
}
|
||||
|
||||
[oids enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
unsigned long oid = [obj unsignedLongValue];
|
||||
LookinDisplayItemDetail *detail = [LookinDisplayItemDetail new];
|
||||
detail.displayItemOid = oid;
|
||||
|
||||
CALayer *layer = (CALayer *)[NSObject lks_objectWithOid:oid];
|
||||
if (![layer isKindOfClass:[CALayer class]]) {
|
||||
block(nil, idx + 1, LookinErr_ObjNotFound);
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
if (idx == 0) {
|
||||
detail.soloScreenshot = [layer lks_soloScreenshotWithLowQuality:lowImageQuality];
|
||||
detail.groupScreenshot = [layer lks_groupScreenshotWithLowQuality:lowImageQuality];
|
||||
} else {
|
||||
detail.groupScreenshot = [layer lks_groupScreenshotWithLowQuality:lowImageQuality];
|
||||
}
|
||||
block(detail, oids.count, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
19
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_CustomAttrModificationHandler.h
generated
Normal file
19
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_CustomAttrModificationHandler.h
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
//
|
||||
// LKS_CustomAttrModificationHandler.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by likaimacbookhome on 2023/11/4.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "LookinCustomAttrModification.h"
|
||||
|
||||
@interface LKS_CustomAttrModificationHandler : NSObject
|
||||
|
||||
/// 返回值表示是否修改成功(有成功调用 setter block 就算成功)
|
||||
+ (BOOL)handleModification:(LookinCustomAttrModification *)modification;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
155
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_CustomAttrModificationHandler.m
generated
Normal file
155
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_CustomAttrModificationHandler.m
generated
Normal file
@@ -0,0 +1,155 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
//
|
||||
// LKS_CustomAttrModificationHandler.m
|
||||
// LookinServer
|
||||
//
|
||||
// Created by likaimacbookhome on 2023/11/4.
|
||||
//
|
||||
|
||||
#import "LKS_CustomAttrModificationHandler.h"
|
||||
#import "LKS_CustomAttrSetterManager.h"
|
||||
#import "UIColor+LookinServer.h"
|
||||
|
||||
@implementation LKS_CustomAttrModificationHandler
|
||||
|
||||
+ (BOOL)handleModification:(LookinCustomAttrModification *)modification {
|
||||
if (!modification || modification.customSetterID.length == 0) {
|
||||
return NO;
|
||||
}
|
||||
switch (modification.attrType) {
|
||||
case LookinAttrTypeNSString: {
|
||||
NSString *newValue = modification.value;
|
||||
if (newValue != nil && ![newValue isKindOfClass:[NSString class]]) {
|
||||
// nil 是合法的
|
||||
return NO;
|
||||
}
|
||||
LKS_StringSetter setter = [[LKS_CustomAttrSetterManager sharedInstance] getStringSetterWithID:modification.customSetterID];
|
||||
if (!setter) {
|
||||
return NO;
|
||||
}
|
||||
setter(newValue);
|
||||
return YES;
|
||||
}
|
||||
|
||||
case LookinAttrTypeDouble: {
|
||||
NSNumber *newValue = modification.value;
|
||||
if (![newValue isKindOfClass:[NSNumber class]]) {
|
||||
return NO;
|
||||
}
|
||||
LKS_NumberSetter setter = [[LKS_CustomAttrSetterManager sharedInstance] getNumberSetterWithID:modification.customSetterID];
|
||||
if (!setter) {
|
||||
return NO;
|
||||
}
|
||||
setter(newValue);
|
||||
return YES;
|
||||
}
|
||||
|
||||
case LookinAttrTypeBOOL: {
|
||||
NSNumber *newValue = modification.value;
|
||||
if (![newValue isKindOfClass:[NSNumber class]]) {
|
||||
return NO;
|
||||
}
|
||||
LKS_BoolSetter setter = [[LKS_CustomAttrSetterManager sharedInstance] getBoolSetterWithID:modification.customSetterID];
|
||||
if (!setter) {
|
||||
return NO;
|
||||
}
|
||||
setter(newValue.boolValue);
|
||||
return YES;
|
||||
}
|
||||
|
||||
case LookinAttrTypeUIColor: {
|
||||
LKS_ColorSetter setter = [[LKS_CustomAttrSetterManager sharedInstance] getColorSetterWithID:modification.customSetterID];
|
||||
if (!setter) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> *newValue = modification.value;
|
||||
if (newValue == nil) {
|
||||
// nil 是合法的
|
||||
setter(nil);
|
||||
return YES;
|
||||
}
|
||||
if (![newValue isKindOfClass:[NSArray class]]) {
|
||||
return NO;
|
||||
}
|
||||
UIColor *color = [UIColor lks_colorFromRGBAComponents:newValue];
|
||||
if (!color) {
|
||||
return NO;
|
||||
}
|
||||
setter(color);
|
||||
return YES;
|
||||
}
|
||||
|
||||
case LookinAttrTypeEnumString: {
|
||||
NSString *newValue = modification.value;
|
||||
if (![newValue isKindOfClass:[NSString class]]) {
|
||||
return NO;
|
||||
}
|
||||
LKS_EnumSetter setter = [[LKS_CustomAttrSetterManager sharedInstance] getEnumSetterWithID:modification.customSetterID];
|
||||
if (!setter) {
|
||||
return NO;
|
||||
}
|
||||
setter(newValue);
|
||||
return YES;
|
||||
}
|
||||
|
||||
case LookinAttrTypeCGRect: {
|
||||
NSValue *newValue = modification.value;
|
||||
if (![newValue isKindOfClass:[NSValue class]]) {
|
||||
return NO;
|
||||
}
|
||||
LKS_RectSetter setter = [[LKS_CustomAttrSetterManager sharedInstance] getRectSetterWithID:modification.customSetterID];
|
||||
if (!setter) {
|
||||
return NO;
|
||||
}
|
||||
setter(newValue.CGRectValue);
|
||||
return YES;
|
||||
}
|
||||
|
||||
case LookinAttrTypeCGSize: {
|
||||
NSValue *newValue = modification.value;
|
||||
if (![newValue isKindOfClass:[NSValue class]]) {
|
||||
return NO;
|
||||
}
|
||||
LKS_SizeSetter setter = [[LKS_CustomAttrSetterManager sharedInstance] getSizeSetterWithID:modification.customSetterID];
|
||||
if (!setter) {
|
||||
return NO;
|
||||
}
|
||||
setter(newValue.CGSizeValue);
|
||||
return YES;
|
||||
}
|
||||
|
||||
case LookinAttrTypeCGPoint: {
|
||||
NSValue *newValue = modification.value;
|
||||
if (![newValue isKindOfClass:[NSValue class]]) {
|
||||
return NO;
|
||||
}
|
||||
LKS_PointSetter setter = [[LKS_CustomAttrSetterManager sharedInstance] getPointSetterWithID:modification.customSetterID];
|
||||
if (!setter) {
|
||||
return NO;
|
||||
}
|
||||
setter(newValue.CGPointValue);
|
||||
return YES;
|
||||
}
|
||||
|
||||
case LookinAttrTypeUIEdgeInsets: {
|
||||
NSValue *newValue = modification.value;
|
||||
if (![newValue isKindOfClass:[NSValue class]]) {
|
||||
return NO;
|
||||
}
|
||||
LKS_InsetsSetter setter = [[LKS_CustomAttrSetterManager sharedInstance] getInsetsSetterWithID:modification.customSetterID];
|
||||
if (!setter) {
|
||||
return NO;
|
||||
}
|
||||
setter(newValue.UIEdgeInsetsValue);
|
||||
return YES;
|
||||
}
|
||||
|
||||
default:
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
30
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_HierarchyDetailsHandler.h
generated
Normal file
30
Pods/LookinServer/Src/Main/Server/Connection/RequestHandler/LKS_HierarchyDetailsHandler.h
generated
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifdef SHOULD_COMPILE_LOOKIN_SERVER
|
||||
|
||||
//
|
||||
// LKS_HierarchyDetailsHandler.h
|
||||
// LookinServer
|
||||
//
|
||||
// Created by Li Kai on 2019/6/20.
|
||||
// https://lookin.work
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class LookinDisplayItemDetail, LookinStaticAsyncUpdateTasksPackage;
|
||||
|
||||
typedef void (^LKS_HierarchyDetailsHandler_ProgressBlock)(NSArray<LookinDisplayItemDetail *> *details);
|
||||
typedef void (^LKS_HierarchyDetailsHandler_FinishBlock)(void);
|
||||
|
||||
@interface LKS_HierarchyDetailsHandler : NSObject
|
||||
|
||||
/// packages 会按照 idx 从小到大的顺序被执行
|
||||
/// 全部任务完成时,finishBlock 会被调用
|
||||
/// 如果调用了 cancel,则 finishBlock 不会被执行
|
||||
- (void)startWithPackages:(NSArray<LookinStaticAsyncUpdateTasksPackage *> *)packages block:(LKS_HierarchyDetailsHandler_ProgressBlock)progressBlock finishedBlock:(LKS_HierarchyDetailsHandler_FinishBlock)finishBlock;
|
||||
|
||||
/// 取消所有任务
|
||||
- (void)cancel;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SHOULD_COMPILE_LOOKIN_SERVER */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user