Compare commits

...

47 Commits

Author SHA1 Message Date
5af2612ff7 fix 2025-11-03 20:02:11 +08:00
cac2f13b88 修改引导逻辑 2025-11-03 19:00:47 +08:00
edf88721da 调整逻辑 2025-11-03 18:45:06 +08:00
915b329805 1 2025-11-03 16:57:24 +08:00
1673a2f4be 添加多语言 2025-11-03 16:37:28 +08:00
e4cebeac85 处理再次进入弹起权限弹窗 2025-11-03 15:04:19 +08:00
c7021e382e fixUI 2025-11-03 13:25:41 +08:00
ffea9d2022 修改KBAuthSession主程序添加token extension没有拿到的情况 2025-10-31 16:50:15 +08:00
90c1e7ff6c 添加token管理 2025-10-31 16:06:54 +08:00
59d04bb33c extension添加提示 2025-10-31 15:08:30 +08:00
eb0d3aaa71 提取baseurl配置 2025-10-31 13:05:34 +08:00
10dfe9b1d6 处理通用链接宏控制 2025-10-30 20:53:44 +08:00
6993bfd682 3 2025-10-30 20:46:54 +08:00
247a87891e 2 2025-10-30 20:23:34 +08:00
9af91cc4bc 1 2025-10-30 18:31:12 +08:00
4f23118ec0 修改逻辑 2025-10-30 18:28:36 +08:00
482756f6f0 优化apple sign 2025-10-30 14:35:06 +08:00
85a3694e35 apple login 2025-10-30 14:29:11 +08:00
f58bf61500 1 2025-10-30 14:04:55 +08:00
783d088f22 处理UI 2025-10-30 13:57:34 +08:00
74476cd592 修改功能 2025-10-30 13:27:09 +08:00
9b43274e93 测试网络,修改UI 2025-10-30 13:10:33 +08:00
8ce1d95c8c 添加图片库、封装 2025-10-29 20:57:45 +08:00
e8c88a6148 修改hud 2025-10-29 19:13:35 +08:00
e218c1bf3d 添加color 2025-10-29 18:25:52 +08:00
c5326a3079 移动文件 2025-10-29 16:44:00 +08:00
9101ffaab0 添加指引 2025-10-29 16:26:57 +08:00
e594711fa3 添加轮询粘贴 2025-10-29 15:49:43 +08:00
7fd084e529 1 2025-10-29 15:02:37 +08:00
11b25241bf basetableview封装 2025-10-29 14:49:35 +08:00
8fcfce7376 添加DZNEmptyDataSet 2025-10-29 14:30:19 +08:00
23317c9fd4 添加基本页面 2025-10-29 14:28:57 +08:00
045d5eaff8 创建base 2025-10-29 14:17:26 +08:00
72b6dbb157 添加MBP 2025-10-29 13:25:46 +08:00
e78b56e2cb 修改UI 2025-10-29 12:59:22 +08:00
6c05026402 fix 2025-10-28 21:27:01 +08:00
13facba33a 4 2025-10-28 20:11:40 +08:00
935284388c 数字面板#+=/123”切换 2025-10-28 20:03:43 +08:00
4c7fd9049f 默认小写 2025-10-28 19:24:35 +08:00
0031b7a5f6 添加设置 2025-10-28 18:02:10 +08:00
02dd204744 处理网络请求 2025-10-28 16:11:35 +08:00
f28f7de49d pod 网络库 2025-10-28 15:59:09 +08:00
c2859f888a 添加网络库,修改问题 2025-10-28 15:34:19 +08:00
a2b51189aa 添加功能组件 2025-10-28 15:18:12 +08:00
2f2f20cfc2 2 2025-10-28 15:10:38 +08:00
377e88b6db 1 2025-10-28 14:30:03 +08:00
1deca2ae5b 1 2025-10-28 10:18:10 +08:00
313 changed files with 28630 additions and 5049 deletions

View 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>

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,415 +0,0 @@
# Masonry [![Build Status](https://travis-ci.org/SnapKit/Masonry.svg?branch=master)](https://travis-ci.org/SnapKit/Masonry) [![Coverage Status](https://img.shields.io/coveralls/SnapKit/Masonry.svg?style=flat-square)](https://coveralls.io/r/SnapKit/Masonry) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![Pod Version](https://img.shields.io/cocoapods/v/Masonry.svg?style=flat)
**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&rsquo;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

View 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

View 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

View 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

View 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

View File

@@ -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 */

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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; // BarView
}
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
// 宿 UIInputViewControllerKeyboardViewController
- (UIInputViewController *)findInputViewController {
UIResponder *responder = self;
while (responder) {
if ([responder isKindOfClass:[UIInputViewController class]]) {
return (UIInputViewController *)responder;
}
responder = responder.nextResponder;
}
return nil;
}
@end

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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
View 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
View File

@@ -0,0 +1,296 @@
DZNEmptyDataSet
=================
[![Pod Version](http://img.shields.io/cocoapods/v/DZNEmptyDataSet.svg)](http://cocoadocs.org/docsets/DZNEmptyDataSet/)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](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/).
![Screenshots_Row1](https://raw.githubusercontent.com/dzenbot/UITableView-DataSet/master/Examples/Applications/Screenshots/Screenshots_row1.png)
![Screenshots_Row2](https://raw.githubusercontent.com/dzenbot/UITableView-DataSet/master/Examples/Applications/Screenshots/Screenshots_row2.png)
(*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.

View 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

File diff suppressed because it is too large Load Diff

21
Pods/LookinServer/LICENSE generated Normal file
View 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
View File

@@ -0,0 +1,73 @@
![Preview](https://cdn.lookin.work/public/style/images/independent/homepage/preview_en_1x.jpg "Preview")
# Introduction
You can inspect and modify views in iOS app via Lookin, just like UI Inspector in Xcode, or another app called Reveal.
Official Websitehttps://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 端 LookinServerhttps://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

View 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 会有一个 LookinIvarTracehostType 为 “superview”hostClassName 为 A 的 classivarName 为 “_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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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];
}
// 0F0F
+ (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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 */

View 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 里的 subItemidx 较大的应该为 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 */

View 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 */

View 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 */

View 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 */

View 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