[YogaKit] support macOS, tvOS, Carthage; auto apply layout like AutoLayout. #1026

Closed
cntrump wants to merge 38 commits from yogakit_autoapplylayout_patch into main
13 changed files with 200 additions and 76 deletions
Showing only changes of commit 14f7e8bb2c - Show all commits

View File

@@ -5,7 +5,7 @@
Pod::Spec.new do |spec|
spec.name = 'Yoga'
spec.version = '1.14.0'
spec.version = '1.14.1'
spec.license = { :type => 'MIT', :file => "LICENSE" }
spec.homepage = 'https://yogalayout.com/'
spec.documentation_url = 'https://yogalayout.com/docs'
@@ -34,5 +34,5 @@ Pod::Spec.new do |spec|
]
spec.source_files = 'yoga/**/*.{c,h,cpp}'
spec.public_header_files = 'yoga/{Yoga,YGEnums,YGMacros,YGNode,YGStyle,YGValue}.h'
spec.static_framework = true
end

View File

@@ -23,7 +23,7 @@ podspec = Pod::Spec.new do |spec|
spec.ios.deployment_target = '8.0'
spec.ios.frameworks = 'UIKit'
spec.module_name = 'YogaKit'
spec.dependency 'Yoga', '~> 1.14'
spec.dependency 'Yoga', '~> 1.14.1'
# Fixes the bug related the xcode 11 not able to find swift related frameworks.
# https://github.com/Carthage/Carthage/issues/2825
# https://twitter.com/krzyzanowskim/status/1151549874653081601?s=21
@@ -32,6 +32,7 @@ podspec = Pod::Spec.new do |spec|
spec.public_header_files = 'YogaKit/Source/{YGLayout,UIView+Yoga}.h'
spec.private_header_files = 'YogaKit/Source/YGLayout+Private.h'
spec.swift_version = '5.1'
spec.static_framework = true
end
# See https://github.com/facebook/yoga/pull/366

View File

@@ -34,4 +34,9 @@ typedef void (^YGLayoutConfigurationBlock)(YGLayout* layout);
@end
@interface UIView (YogaKitAutoApplyLayout)
@end
NS_ASSUME_NONNULL_END

View File

@@ -35,3 +35,88 @@ static const void* kYGYogaAssociatedKey = &kYGYogaAssociatedKey;
}
@end
static const void* kYGBoundsSizeAssociatedKey = &kYGBoundsSizeAssociatedKey;
static void YogaSwizzleInstanceMethod(Class cls, SEL originalSelector, SEL swizzledSelector);
@implementation UIView (YogaKitAutoApplyLayout)
+ (void)load {
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
YogaSwizzleInstanceMethod(self, @selector(initWithFrame:), @selector(_yoga_initWithFrame:));
YogaSwizzleInstanceMethod(self, @selector(setFrame:), @selector(_yoga_setFrame:));
YogaSwizzleInstanceMethod(self, @selector(setBounds:), @selector(_yoga_setBounds:));
});
}
- (CGSize)_yoga_boundsSize {
NSValue *value = (NSValue *)objc_getAssociatedObject(self, kYGBoundsSizeAssociatedKey);
return value ? value.CGSizeValue : CGSizeMake(YGUndefined, YGUndefined);
}
- (void)set_yoga_boundsSize:(CGSize)size {
objc_setAssociatedObject(self,
kYGBoundsSizeAssociatedKey,
[NSValue valueWithCGSize:size],
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (instancetype)_yoga_initWithFrame:(CGRect)frame {
id _self = [self _yoga_initWithFrame:frame];
if (_self) {
[self _yoga_applyLayout];
}
return _self;
}
- (void)_yoga_setFrame:(CGRect)frame {
[self _yoga_setFrame:frame];
[self _yoga_applyLayout];
}
- (void)_yoga_setBounds:(CGRect)bounds {
[self _yoga_setBounds:bounds];
[self _yoga_applyLayout];
}
- (void)_yoga_applyLayout {
if (self.isYogaEnabled && self.yoga.isEnabled) {
CGSize size = self.bounds.size;
CGSize prev = self._yoga_boundsSize;
if (!CGSizeEqualToSize(size, prev)) {
self._yoga_boundsSize = size;
[self.yoga applyLayoutPreservingOrigin:YES];
}
}
}
@end
static void YogaSwizzleInstanceMethod(Class cls, SEL originalSelector, SEL swizzledSelector) {
if (!cls || !originalSelector || !swizzledSelector) {
return;
}
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
if (!originalMethod || !swizzledMethod) {
return;
}
IMP swizzledIMP = method_getImplementation(swizzledMethod);
if (class_addMethod(cls, originalSelector, swizzledIMP, method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(cls,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

View File

@@ -162,6 +162,7 @@ static YGConfigRef globalConfig;
@property(nonatomic, weak, readonly) UIView* view;
@property(nonatomic, assign, readonly) BOOL isUIView;
@property(nonatomic, assign) BOOL isApplingLayout;
@end
@@ -292,11 +293,19 @@ YG_PROPERTY(CGFloat, aspectRatio, AspectRatio)
}
- (void)applyLayout {
if (self.isApplingLayout) {
return;
}
[self calculateLayoutWithSize:self.view.bounds.size];
YGApplyLayoutToViewHierarchy(self.view, NO);
}
- (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin {
if (self.isApplingLayout) {
return;
}
[self calculateLayoutWithSize:self.view.bounds.size];
YGApplyLayoutToViewHierarchy(self.view, preserveOrigin);
}
@@ -304,6 +313,10 @@ YG_PROPERTY(CGFloat, aspectRatio, AspectRatio)
- (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin
dimensionFlexibility:
(YGDimensionFlexibility)dimensionFlexibility {
if (self.isApplingLayout) {
return;
}
CGSize size = self.view.bounds.size;
if (dimensionFlexibility & YGDimensionFlexibilityFlexibleWidth) {
size.width = YGUndefined;
@@ -463,12 +476,22 @@ static void YGApplyLayoutToViewHierarchy(UIView* view, BOOL preserveOrigin) {
[NSThread isMainThread],
@"Framesetting should only be done on the main thread.");
if (!view.isYogaEnabled || !view.yoga.isEnabled) {
return;
}
const YGLayout* yoga = view.yoga;
if (yoga.isApplingLayout) {
return;
}
if (!yoga.isIncludedInLayout) {
return;
}
yoga.isApplingLayout = YES;
YGNodeRef node = yoga.node;
const CGPoint topLeft = {
YGNodeLayoutGetLeft(node),
@@ -481,7 +504,7 @@ static void YGApplyLayoutToViewHierarchy(UIView* view, BOOL preserveOrigin) {
};
const CGPoint origin = preserveOrigin ? view.frame.origin : CGPointZero;
view.frame = (CGRect){
CGRect frame = (CGRect){
.origin =
{
.x = YGRoundPixelValue(topLeft.x + origin.x),
@@ -489,18 +512,31 @@ static void YGApplyLayoutToViewHierarchy(UIView* view, BOOL preserveOrigin) {
},
.size =
{
.width = YGRoundPixelValue(bottomRight.x) -
YGRoundPixelValue(topLeft.x),
.height = YGRoundPixelValue(bottomRight.y) -
YGRoundPixelValue(topLeft.y),
.width = MAX(YGRoundPixelValue(bottomRight.x) -
YGRoundPixelValue(topLeft.x), 0),
.height = MAX(YGRoundPixelValue(bottomRight.y) -
YGRoundPixelValue(topLeft.y), 0),
},
};
// use bounds/center and not frame if non-identity transform.
view.bounds = (CGRect) {
.origin = view.bounds.origin,
.size = frame.size
};
view.center = (CGPoint) {
.x = YGRoundPixelValue(CGRectGetMinX(frame) + CGRectGetWidth(frame) * 0.5),
.y = YGRoundPixelValue(CGRectGetMinY(frame) + CGRectGetHeight(frame) * 0.5)
};
if (!yoga.isLeaf) {
for (NSUInteger i = 0; i < view.subviews.count; i++) {
YGApplyLayoutToViewHierarchy(view.subviews[i], NO);
}
}
yoga.isApplingLayout = NO;
}
@end

View File

@@ -1,6 +1,8 @@
platform :ios, '9.0'
use_frameworks!
target 'YogaKitSample' do
pod 'Yoga', :path => '../../Yoga.podspec'
pod 'YogaKit', :path => '../../YogaKit.podspec'
pod 'IGListKit', '~> 2.1.0'
pod 'IGListKit', '~> 4.0.0'
end

View File

@@ -1,26 +1,33 @@
PODS:
- IGListKit (2.1.0):
- IGListKit/Default (= 2.1.0)
- IGListKit/Default (2.1.0):
- IGListKit/Diffing
- IGListKit/Diffing (2.1.0)
- Yoga (1.7.0)
- YogaKit (1.7.0):
- Yoga (~> 1.7)
- IGListDiffKit (4.0.0)
- IGListKit (4.0.0):
- IGListDiffKit (= 4.0.0)
- Yoga (1.14.1)
- YogaKit (1.18.1):
- Yoga (~> 1.14.1)
DEPENDENCIES:
- IGListKit (~> 2.1.0)
- IGListKit (~> 4.0.0)
- Yoga (from `../../Yoga.podspec`)
- YogaKit (from `../../YogaKit.podspec`)
SPEC REPOS:
trunk:
- IGListDiffKit
- IGListKit
EXTERNAL SOURCES:
Yoga:
:path: "../../Yoga.podspec"
YogaKit:
:path: ../../YogaKit.podspec
:path: "../../YogaKit.podspec"
SPEC CHECKSUMS:
IGListKit: b826c68ef7a4ae1626c09d4d3e1ea7a169e6c36e
Yoga: 2ed1d7accfef3610a67f58c0cf101a0662137f2c
YogaKit: 31576530e8fcae3175469719ec3212397403330b
IGListDiffKit: 665d6cf43ce726e676013db9c7d6c4294259b6b2
IGListKit: fd5a5d21935298f5849fa49d426843cff97b77c7
Yoga: 1b62f19e549f5e174fc935ba76a47a8d9b60211e
YogaKit: befe87ed72becf4125f290a33af021e9992712c7
PODFILE CHECKSUM: 216f8e7127767709e0e43f3711208d238fa5c404
PODFILE CHECKSUM: 31f0f3ccdee77adc2f4b58b4772186c206fd1446
COCOAPODS: 1.1.1
COCOAPODS: 1.9.1

View File

@@ -1,10 +1,3 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// !$*UTF8*$!
{
archiveVersion = 1;
@@ -178,7 +171,6 @@
13687D401DF8748300E7C260 /* Frameworks */,
13687D411DF8748300E7C260 /* Resources */,
FA2FB9DD6471BDD3FBCE503B /* [CP] Embed Pods Frameworks */,
6E01EB987F1564F3D71EBE5A /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -233,6 +225,7 @@
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
Base,
);
@@ -272,28 +265,16 @@
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-YogaKitSample-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
showEnvVarsInLog = 0;
};
6E01EB987F1564F3D71EBE5A /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-YogaKitSample/Pods-YogaKitSample-resources.sh\"\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
FA2FB9DD6471BDD3FBCE503B /* [CP] Embed Pods Frameworks */ = {
@@ -302,13 +283,18 @@
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-YogaKitSample/Pods-YogaKitSample-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/IGListDiffKit/IGListDiffKit.framework",
"${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListDiffKit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListKit.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-YogaKitSample/Pods-YogaKitSample-frameworks.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-YogaKitSample/Pods-YogaKitSample-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@@ -446,7 +432,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_INSTALL_OBJC_HEADER = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 3.0;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@@ -461,7 +447,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.facebook.YogaKitSample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_INSTALL_OBJC_HEADER = NO;
SWIFT_VERSION = 3.0;
SWIFT_VERSION = 5.0;
};
name = Release;
};

View File

@@ -18,12 +18,12 @@ private final class ExampleModel {
}
}
extension ExampleModel: IGListDiffable {
extension ExampleModel: ListDiffable {
fileprivate func diffIdentifier() -> NSObjectProtocol {
return title as NSString
}
fileprivate func isEqual(toDiffableObject object: IGListDiffable?) -> Bool {
fileprivate func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
guard let otherObj = object as? ExampleModel else { return false }
return (title == otherObj.title) &&
@@ -31,11 +31,11 @@ extension ExampleModel: IGListDiffable {
}
}
final class ExamplesViewController: UIViewController, IGListAdapterDataSource, IGListSingleSectionControllerDelegate {
private lazy var adapter: IGListAdapter = {
return IGListAdapter(updater: IGListAdapterUpdater(), viewController: self, workingRangeSize: 0)
final class ExamplesViewController: UIViewController, ListAdapterDataSource, ListSingleSectionControllerDelegate {
private lazy var adapter: ListAdapter = {
return ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
}()
private let collectionView = IGListCollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
private let collectionView = ListCollectionView(frame: .zero)
// Update this to array to create more examples.
@@ -47,6 +47,7 @@ final class ExamplesViewController: UIViewController, IGListAdapterDataSource, I
override func viewDidLoad() {
super.viewDidLoad()
title = "Examples"
collectionView.backgroundColor = .clear
view.addSubview(collectionView)
adapter.collectionView = collectionView
adapter.dataSource = self
@@ -59,16 +60,16 @@ final class ExamplesViewController: UIViewController, IGListAdapterDataSource, I
//MARK: IGListAdapterDataSource
func objects(for listAdapter: IGListAdapter) -> [IGListDiffable] {
return models as [IGListDiffable]
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return models as [ListDiffable]
}
func listAdapter(_ listAdapter: IGListAdapter, sectionControllerFor object: Any) -> IGListSectionController {
let sizeBlock: IGListSingleSectionCellSizeBlock = { (model, context) in
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
let sizeBlock: ListSingleSectionCellSizeBlock = { (model, context) in
return CGSize(width: (context?.containerSize.width)!, height: 75.0)
}
let configureBlock: IGListSingleSectionCellConfigureBlock = { (model, cell) in
let configureBlock: ListSingleSectionCellConfigureBlock = { (model, cell) in
guard let m = model as? ExampleModel, let c = cell as? SingleLabelCollectionCell else {
return
}
@@ -76,18 +77,18 @@ final class ExamplesViewController: UIViewController, IGListAdapterDataSource, I
c.label.text = m.title
}
let sectionController = IGListSingleSectionController(cellClass: SingleLabelCollectionCell.self,
let sectionController = ListSingleSectionController(cellClass: SingleLabelCollectionCell.self,
configureBlock: configureBlock,
sizeBlock: sizeBlock)
sectionController.selectionDelegate = self
return sectionController
}
func emptyView(for listAdapter: IGListAdapter) -> UIView? { return nil }
func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil }
//MARK: IGListSingleSectionControllerDelegate
func didSelect(_ sectionController: IGListSingleSectionController) {
func didSelect(_ sectionController: ListSingleSectionController, with object: Any) {
let section = adapter.section(for: sectionController)
let model = models[section]

View File

@@ -32,5 +32,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
</dict>
</plist>

View File

@@ -32,22 +32,24 @@ final class BasicViewController: UIViewController {
}
root.addSubview(child1)
let child2 = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let child2 = UIView()
child2.backgroundColor = .green
child2.configureLayout { (layout) in
layout.isEnabled = true
layout.alignSelf = .flexEnd
layout.width = 200
layout.height = 200
}
root.addSubview(child2)
let child3 = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let child3 = UIView()
child3.backgroundColor = .yellow
child3.configureLayout { (layout) in
layout.isEnabled = true
layout.alignSelf = .flexStart
layout.width = 100
layout.height = 100
}
root.addSubview(child3)
root.yoga.applyLayout(preservingOrigin: true)
}
}

View File

@@ -51,9 +51,9 @@ final class LayoutInclusionViewController: UIViewController {
}
contentView.addSubview(disappearingView)
button.setTitle("Add Blue View", for: UIControlState.selected)
button.setTitle("Remove Blue View", for: UIControlState.normal)
button.addTarget(self, action: #selector(buttonWasTapped), for: UIControlEvents.touchUpInside)
button.setTitle("Add Blue View", for: .selected)
button.setTitle("Remove Blue View", for: .normal)
button.addTarget(self, action: #selector(buttonWasTapped), for: UIControl.Event.touchUpInside)
button.configureLayout { (layout) in
layout.isEnabled = true
layout.height = 300
@@ -61,12 +61,10 @@ final class LayoutInclusionViewController: UIViewController {
layout.alignSelf = .center
}
root.addSubview(button)
root.yoga.applyLayout(preservingOrigin: false)
}
// MARK - UIButton Action
func buttonWasTapped() {
@objc func buttonWasTapped() {
button.isSelected = !button.isSelected
button.isUserInteractionEnabled = false

View File

@@ -42,7 +42,6 @@ final class SingleLabelCollectionCell: UICollectionViewCell {
override func layoutSubviews() {
super.layoutSubviews()
contentView.yoga.applyLayout(preservingOrigin: false)
label.frame = contentView.bounds
}
}