Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@ fastlane/test_output
iOSInjectionProject/

.build
.tmp
10 changes: 10 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 0.5.0

1. 新增 `ZDFlexLayoutAsyncMode` 枚举,支持三种布局计算模式:同步、RunLoop Idle、后台线程
2. 后台线程模式采用"缓存侧表"方案:主线程预测量 → 后台纯数值计算 → 主线程刷新,不污染 YGNode style
3. 新增 `applyLayoutWithAsyncMode:preservingOrigin:dimensionFlexibility:` 统一异步布局 API
4. 新增 `useLegacyPreMeasure` 属性,可切换回旧版预测量实现作为备用
5. 修复 RunLoop Idle 模式下 weakSelf 未做 nil 判断的问题
6. 文本测量统一使用 TextKit(`NSTextStorage + NSLayoutManager + NSTextContainer`),支持多行文本在后台线程正确计算高度
7. 修复 `isUIView`(`isMemberOfClass:`)导致 UILabel 等子类走错测量分支的问题

## 0.4.0

1. `Yoga`更新到`0.3.2.1`
Expand Down
10 changes: 10 additions & 0 deletions Demo/Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
FA843035234F4234000F7E35 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FA843033234F4234000F7E35 /* LaunchScreen.storyboard */; };
FA843038234F4234000F7E35 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = FA843037234F4234000F7E35 /* main.m */; };
FA843042234F4234000F7E35 /* DemoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FA843041234F4234000F7E35 /* DemoTests.m */; };
FA843042234F4234000F7E36 /* AsyncLayoutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FA843041234F4234000F7E36 /* AsyncLayoutTests.m */; };
FA84304D234F4234000F7E35 /* DemoUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = FA84304C234F4234000F7E35 /* DemoUITests.m */; };
FA90B083237D4D2600DBDB77 /* NormalLayoutController.m in Sources */ = {isa = PBXBuildFile; fileRef = FA90B082237D4D2600DBDB77 /* NormalLayoutController.m */; };
FA90B0A0237D4D2600DBDB77 /* AsyncLayoutController.m in Sources */ = {isa = PBXBuildFile; fileRef = FA90B09F237D4D2600DBDB77 /* AsyncLayoutController.m */; };
FA90B086237D4D3E00DBDB77 /* ScrollViewLayoutController.m in Sources */ = {isa = PBXBuildFile; fileRef = FA90B085237D4D3E00DBDB77 /* ScrollViewLayoutController.m */; };
FACF10A425B70AEC00C91DA3 /* Empty.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF10A325B70AEC00C91DA3 /* Empty.swift */; };
FAEA4A0F235F0F6000422C35 /* same_city_users.json in Resources */ = {isa = PBXBuildFile; fileRef = FAEA4A0E235F0F6000422C35 /* same_city_users.json */; };
Expand Down Expand Up @@ -94,12 +96,15 @@
FA843037234F4234000F7E35 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
FA84303D234F4234000F7E35 /* DemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
FA843041234F4234000F7E35 /* DemoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DemoTests.m; sourceTree = "<group>"; };
FA843041234F4234000F7E36 /* AsyncLayoutTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AsyncLayoutTests.m; sourceTree = "<group>"; };
FA843043234F4234000F7E35 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
FA843048234F4234000F7E35 /* DemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
FA84304C234F4234000F7E35 /* DemoUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DemoUITests.m; sourceTree = "<group>"; };
FA84304E234F4234000F7E35 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
FA90B081237D4D2600DBDB77 /* NormalLayoutController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NormalLayoutController.h; sourceTree = "<group>"; };
FA90B082237D4D2600DBDB77 /* NormalLayoutController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NormalLayoutController.m; sourceTree = "<group>"; };
FA90B09E237D4D2600DBDB77 /* AsyncLayoutController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncLayoutController.h; sourceTree = "<group>"; };
FA90B09F237D4D2600DBDB77 /* AsyncLayoutController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AsyncLayoutController.m; sourceTree = "<group>"; };
FA90B084237D4D3E00DBDB77 /* ScrollViewLayoutController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScrollViewLayoutController.h; sourceTree = "<group>"; };
FA90B085237D4D3E00DBDB77 /* ScrollViewLayoutController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScrollViewLayoutController.m; sourceTree = "<group>"; };
FACF10A325B70AEC00C91DA3 /* Empty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Empty.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -245,6 +250,7 @@
isa = PBXGroup;
children = (
FA843041234F4234000F7E35 /* DemoTests.m */,
FA843041234F4234000F7E36 /* AsyncLayoutTests.m */,
FA843043234F4234000F7E35 /* Info.plist */,
);
path = DemoTests;
Expand All @@ -266,6 +272,8 @@
FA90B082237D4D2600DBDB77 /* NormalLayoutController.m */,
FA90B084237D4D3E00DBDB77 /* ScrollViewLayoutController.h */,
FA90B085237D4D3E00DBDB77 /* ScrollViewLayoutController.m */,
FA90B09E237D4D2600DBDB77 /* AsyncLayoutController.h */,
FA90B09F237D4D2600DBDB77 /* AsyncLayoutController.m */,
);
path = LayoutDemo;
sourceTree = "<group>";
Expand Down Expand Up @@ -557,6 +565,7 @@
FAEA4A13235F0FD400422C35 /* SameCityUserListController.m in Sources */,
FAEA4A16235F0FF600422C35 /* SameCityUserListViewModel.m in Sources */,
FA90B083237D4D2600DBDB77 /* NormalLayoutController.m in Sources */,
FA90B0A0237D4D2600DBDB77 /* AsyncLayoutController.m in Sources */,
FAEA4A19235F104A00422C35 /* UserModel.m in Sources */,
FA617C0C23572AA100477C31 /* ZDFlexCell.m in Sources */,
FA1A54DF2355ECE400C80712 /* YogaKitListViewModel.m in Sources */,
Expand All @@ -575,6 +584,7 @@
buildActionMask = 2147483647;
files = (
FA843042234F4234000F7E35 /* DemoTests.m in Sources */,
FA843042234F4234000F7E36 /* AsyncLayoutTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
17 changes: 17 additions & 0 deletions Demo/Demo/Sample/LayoutDemo/AsyncLayoutController.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// AsyncLayoutController.h
// Demo
//
// Created by Zero.D.Saber on 2024/12/1.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

/// Demonstrates ZDFlexLayoutAsyncMode: Sync, RunLoop Idle, and BackgroundThread modes.
@interface AsyncLayoutController : UIViewController

@end

NS_ASSUME_NONNULL_END
261 changes: 261 additions & 0 deletions Demo/Demo/Sample/LayoutDemo/AsyncLayoutController.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
//
// AsyncLayoutController.m
// Demo
//
// Created by Zero.D.Saber on 2024/12/1.
//

#import "AsyncLayoutController.h"
@import ZDFlexLayoutKit;

@interface AsyncLayoutController ()

@property (nonatomic, strong) UIView *syncContainer;
@property (nonatomic, strong) UIView *idleContainer;
@property (nonatomic, strong) UIView *asyncContainer;
@property (nonatomic, strong) UISegmentedControl *modeSegment;

@end

@implementation AsyncLayoutController

- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Async Layout Demo";
self.view.backgroundColor = UIColor.whiteColor;

[self setupModeSelector];
[self buildLayoutWithMode:ZDFlexLayoutAsyncModeSync];
}

#pragma mark - Mode Selector

- (void)setupModeSelector {
self.modeSegment = [[UISegmentedControl alloc] initWithItems:@[@"Sync", @"RunLoop Idle", @"Background Thread"]];
self.modeSegment.selectedSegmentIndex = 0;
[self.modeSegment addTarget:self action:@selector(modeChanged:) forControlEvents:UIControlEventValueChanged];
self.modeSegment.frame = CGRectMake(20, 120, self.view.bounds.size.width - 40, 36);
[self.view addSubview:self.modeSegment];
}

- (void)modeChanged:(UISegmentedControl *)sender {
[self clearLayout];
ZDFlexLayoutAsyncMode mode;
switch (sender.selectedSegmentIndex) {
case 0: mode = ZDFlexLayoutAsyncModeSync; break;
case 1: mode = ZDFlexLayoutAsyncModeRunloopIdle; break;
case 2: mode = ZDFlexLayoutAsyncModeBackgroundThread; break;
default: mode = ZDFlexLayoutAsyncModeSync; break;
}
[self buildLayoutWithMode:mode];
}

#pragma mark - Layout Construction

- (void)clearLayout {
[self.syncContainer removeFromSuperview];
[self.idleContainer removeFromSuperview];
[self.asyncContainer removeFromSuperview];
self.syncContainer = nil;
self.idleContainer = nil;
self.asyncContainer = nil;
}

- (void)buildLayoutWithMode:(ZDFlexLayoutAsyncMode)mode {
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 160, self.view.bounds.size.width, self.view.bounds.size.height - 160)];
container.backgroundColor = UIColor.systemGroupedBackgroundColor;
[self.view addSubview:container];

// Root flex container
[container zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.flexDirection(YGFlexDirectionColumn);
make.padding(YGPointValue(16));
}];

// Title label
UILabel *titleLabel = [self makeLabelWithText:[self titleForMode:mode] fontSize:18 color:UIColor.blackColor];
[titleLabel zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.marginBottom(YGPointValue(12));
}];
[container addChild:titleLabel];

// Card 1: UILabel measurement
UIView *card1 = [self makeCardWithTitle:@"UILabel (text measurement)"
content:@"This label demonstrates that text content is correctly measured in async mode without accessing UIKit on the background thread."];
[container addChild:card1];

// Card 2: UIImageView measurement
UIView *card2 = [self makeImageCard];
[container addChild:card2];

// Card 3: Custom UIView with sizeThatFits
UIView *card3 = [self makeCustomViewCard];
[container addChild:card3];

// Card 4: Mixed layout with flexGrow
UIView *card4 = [self makeFlexGrowCard];
[container addChild:card4];

// Apply layout with selected mode
[container.flexLayout applyLayoutWithAsyncMode:mode
preservingOrigin:YES
dimensionFlexibility:ZDDimensionFlexibilityFlexibleHeight];

switch (mode) {
case ZDFlexLayoutAsyncModeSync:
self.syncContainer = container;
break;
case ZDFlexLayoutAsyncModeRunloopIdle:
self.idleContainer = container;
break;
case ZDFlexLayoutAsyncModeBackgroundThread:
self.asyncContainer = container;
break;
}
}

#pragma mark - Card Builders

- (UIView *)makeCardWithTitle:(NSString *)title content:(NSString *)content {
UIView *card = UIView.new;
card.backgroundColor = UIColor.whiteColor;
card.layer.cornerRadius = 8;
card.clipsToBounds = YES;
[card zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.flexDirection(YGFlexDirectionColumn);
make.padding(YGPointValue(12));
make.marginBottom(YGPointValue(12));
}];

UILabel *titleLabel = [self makeLabelWithText:title fontSize:14 color:UIColor.darkGrayColor];
[titleLabel zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.marginBottom(YGPointValue(6));
}];
[card addChild:titleLabel];

UILabel *contentLabel = [self makeLabelWithText:content fontSize:13 color:UIColor.grayColor];
contentLabel.numberOfLines = 0;
[contentLabel zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
}];
[card addChild:contentLabel];

return card;
}

- (UIView *)makeImageCard {
UIView *card = UIView.new;
card.backgroundColor = UIColor.whiteColor;
card.layer.cornerRadius = 8;
card.clipsToBounds = YES;
[card zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.flexDirection(YGFlexDirectionRow);
make.alignItems(YGAlignCenter);
make.padding(YGPointValue(12));
make.marginBottom(YGPointValue(12));
}];

UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"photo.fill"]];
imageView.tintColor = UIColor.systemBlueColor;
imageView.contentMode = UIViewContentModeScaleAspectFit;
[imageView zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.width(YGPointValue(40)).height(YGPointValue(40));
make.marginRight(YGPointValue(12));
}];
[card addChild:imageView];

UILabel *label = [self makeLabelWithText:@"UIImageView with explicit size (40x40)" fontSize:13 color:UIColor.darkGrayColor];
[label zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES).flexShrink(1);
}];
[card addChild:label];

return card;
}

- (UIView *)makeCustomViewCard {
UIView *card = UIView.new;
card.backgroundColor = UIColor.whiteColor;
card.layer.cornerRadius = 8;
card.clipsToBounds = YES;
[card zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.flexDirection(YGFlexDirectionColumn);
make.padding(YGPointValue(12));
make.marginBottom(YGPointValue(12));
}];

UILabel *titleLabel = [self makeLabelWithText:@"Custom views with sizeThatFits:" fontSize:14 color:UIColor.darkGrayColor];
[titleLabel zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.marginBottom(YGPointValue(8));
}];
[card addChild:titleLabel];

UISwitch *toggle = UISwitch.new;
[toggle zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
}];
[card addChild:toggle];

return card;
}

- (UIView *)makeFlexGrowCard {
UIView *card = UIView.new;
card.backgroundColor = UIColor.whiteColor;
card.layer.cornerRadius = 8;
card.clipsToBounds = YES;
[card zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.flexDirection(YGFlexDirectionRow);
make.padding(YGPointValue(8));
make.marginBottom(YGPointValue(12));
make.height(YGPointValue(60));
}];

NSArray *colors = @[UIColor.systemRedColor, UIColor.systemGreenColor, UIColor.systemBlueColor];
NSArray *grows = @[@1, @2, @1];

for (NSInteger i = 0; i < 3; i++) {
UIView *bar = UIView.new;
bar.backgroundColor = colors[i];
bar.layer.cornerRadius = 4;
CGFloat grow = [grows[i] floatValue];
[bar zd_makeFlexLayout:^(ZDFlexLayoutMaker * _Nonnull make) {
make.isEnabled(YES);
make.flexGrow(grow);
make.marginHorizontal(YGPointValue(4));
}];
[card addChild:bar];
}

return card;
}

#pragma mark - Helpers

- (UILabel *)makeLabelWithText:(NSString *)text fontSize:(CGFloat)size color:(UIColor *)color {
UILabel *label = UILabel.new;
label.text = text;
label.font = [UIFont systemFontOfSize:size];
label.textColor = color;
label.numberOfLines = 0;
return label;
}

- (NSString *)titleForMode:(ZDFlexLayoutAsyncMode)mode {
switch (mode) {
case ZDFlexLayoutAsyncModeSync: return @"Mode: Sync (immediate)";
case ZDFlexLayoutAsyncModeRunloopIdle: return @"Mode: RunLoop Idle";
case ZDFlexLayoutAsyncModeBackgroundThread: return @"Mode: Background Thread";
}
}

@end
17 changes: 16 additions & 1 deletion Demo/Demo/ViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

#import "ViewController.h"
#import "AsyncLayoutController.h"
//#import <ZDFlexLayoutKit/ZDFlexLayoutKit.h>
@import ZDFlexLayoutKit;

Expand All @@ -18,7 +19,21 @@ @implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

UIButton *asyncBtn = [UIButton buttonWithType:UIButtonTypeSystem];
asyncBtn.frame = CGRectMake(43, 430, 343, 50);
asyncBtn.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
asyncBtn.backgroundColor = UIColor.systemGreenColor;
asyncBtn.titleLabel.font = [UIFont systemFontOfSize:18];
[asyncBtn setTitle:@"AsyncLayout" forState:UIControlStateNormal];
[asyncBtn setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[asyncBtn addTarget:self action:@selector(pushAsyncLayout) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:asyncBtn];
}

- (void)pushAsyncLayout {
AsyncLayoutController *vc = [[AsyncLayoutController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}

@end
Loading