mirror of
https://github.com/facebookincubator/QuickLayout
synced 2026-04-21 13:37:22 +00:00
Initial commit
fbshipit-source-id: ac9cb65f0bb8a29acff5b6664ede5c4ce7a1a632
This commit is contained in:
commit
cf25689181
275 changed files with 36505 additions and 0 deletions
69
.github/workflows/docs.yml
vendored
Normal file
69
.github/workflows/docs.yml
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'Sources/QuickLayout/docs/**'
|
||||
# Review gh actions docs if you want to further define triggers, paths, etc
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: ./Sources/QuickLayout/docs
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docusaurus
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: facebook/install-dotslash@latest
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: yarn
|
||||
cache-dependency-path: 'Sources/QuickLayout/docs/package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Disable watchman
|
||||
run: |
|
||||
echo '[buck2]' >> $GITHUB_WORKSPACE/.buckconfig
|
||||
echo 'file_watcher=notify' >> $GITHUB_WORKSPACE/.buckconfig
|
||||
- name: Add repo to PATH
|
||||
run: |
|
||||
echo "$GITHUB_WORKSPACE" >> $GITHUB_PATH
|
||||
- name: Build website
|
||||
run: yarn build
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: Sources/QuickLayout/docs/build
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: build
|
||||
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
71
.gitignore
vendored
Normal file
71
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
.DS_Store
|
||||
|
||||
Podfile.lock
|
||||
Gemfile.lock
|
||||
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData/
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata/
|
||||
|
||||
## Other
|
||||
*.moved-aside
|
||||
*.xcuserstate
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Docs
|
||||
docs/docsets/
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
|
||||
Pods/
|
||||
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
*.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build
|
||||
|
||||
# Bundler
|
||||
.bundle
|
||||
vendor
|
||||
|
||||
# Jetbrains
|
||||
.idea
|
||||
80
CODE_OF_CONDUCT.md
Normal file
80
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to make participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when
|
||||
an individual is representing the project or its community in public spaces.
|
||||
Examples of representing a project or community include using an official
|
||||
project e-mail address, posting via an official social media account, or acting
|
||||
as an appointed representative at an online or offline event. Representation of
|
||||
a project may be further defined and clarified by project maintainers.
|
||||
|
||||
This Code of Conduct also applies outside the project spaces when there is a
|
||||
reasonable belief that an individual's behavior may have a negative impact on
|
||||
the project or its community.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at <opensource-conduct@meta.com>. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
34
CONTRIBUTING.md
Normal file
34
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Contributing to Meta Open Source Projects
|
||||
|
||||
We want to make contributing to this project as easy and transparent as
|
||||
possible.
|
||||
|
||||
## Pull Requests
|
||||
We actively welcome your pull requests.
|
||||
|
||||
Note: pull requests are not imported into the GitHub directory in the usual way. There is an internal Meta repository that is the "source of truth" for the project. The GitHub repository is generated *from* the internal Meta repository. So we don't merge GitHub PRs directly to the GitHub repository -- they must first be imported into internal Meta repository. When Meta employees look at the GitHub PR, there is a special button visible only to them that executes that import. The changes are then automatically reflected from the internal Meta repository back to GitHub. This is why you won't see your PR having being directly merged, but you still see your changes in the repository once it reflects the imported changes.
|
||||
|
||||
1. Fork the repo and create your branch from `main`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've changed APIs, update the documentation.
|
||||
4. Ensure the test suite passes.
|
||||
5. Make sure your code lints.
|
||||
6. If you haven't already, complete the Contributor License Agreement ("CLA").
|
||||
|
||||
## Contributor License Agreement ("CLA")
|
||||
In order to accept your pull request, we need you to submit a CLA. You only need
|
||||
to do this once to work on any of Meta's open source projects.
|
||||
|
||||
Complete your CLA here: <https://code.facebook.com/cla>
|
||||
|
||||
## Issues
|
||||
We use GitHub issues to track public bugs. Please ensure your description is
|
||||
clear and has sufficient instructions to be able to reproduce the issue.
|
||||
|
||||
Meta has a [bounty program](https://www.facebook.com/whitehat/) for the safe
|
||||
disclosure of security bugs. In those cases, please go through the process
|
||||
outlined on that page and do not file a public issue.
|
||||
|
||||
## License
|
||||
By contributing to this project, you agree that your contributions will be licensed
|
||||
under the LICENSE file in the root directory of this source tree.
|
||||
370
Demo/Demo.xcodeproj/project.pbxproj
Normal file
370
Demo/Demo.xcodeproj/project.pbxproj
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0E56B79C2EBCE521004F79FC /* QuickLayoutShowcaseAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E56B79B2EBCE521004F79FC /* QuickLayoutShowcaseAssets.xcassets */; };
|
||||
0E6E97342EB93FDF0030E0DA /* QuickLayout in Frameworks */ = {isa = PBXBuildFile; productRef = 0E6E97332EB93FDF0030E0DA /* QuickLayout */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0E56B79B2EBCE521004F79FC /* QuickLayoutShowcaseAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = QuickLayoutShowcaseAssets.xcassets; path = ../Sources/QuickLayout/QuickLayoutShowcaseAssets.xcassets; sourceTree = SOURCE_ROOT; };
|
||||
0E6E96F72EB93F9C0030E0DA /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
0E18AC1A2EC0EA5E00BC420B /* __showcase__ */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
name = __showcase__;
|
||||
path = ../Sources/QuickLayout/QuickLayout/__showcase__;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
0E6E96F42EB93F9C0030E0DA /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0E6E97342EB93FDF0030E0DA /* QuickLayout in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0E6E96EE2EB93F9C0030E0DA = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E18AC1A2EC0EA5E00BC420B /* __showcase__ */,
|
||||
0E56B79B2EBCE521004F79FC /* QuickLayoutShowcaseAssets.xcassets */,
|
||||
0E6E97322EB93FDF0030E0DA /* Frameworks */,
|
||||
0E6E96F82EB93F9C0030E0DA /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0E6E96F82EB93F9C0030E0DA /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E6E96F72EB93F9C0030E0DA /* Demo.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0E6E97322EB93FDF0030E0DA /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
0E6E96F62EB93F9C0030E0DA /* Demo */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 0E6E97022EB93F9D0030E0DA /* Build configuration list for PBXNativeTarget "Demo" */;
|
||||
buildPhases = (
|
||||
0E6E96F32EB93F9C0030E0DA /* Sources */,
|
||||
0E6E96F42EB93F9C0030E0DA /* Frameworks */,
|
||||
0E6E96F52EB93F9C0030E0DA /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
0E18AC1A2EC0EA5E00BC420B /* __showcase__ */,
|
||||
);
|
||||
name = Demo;
|
||||
packageProductDependencies = (
|
||||
0E6E97332EB93FDF0030E0DA /* QuickLayout */,
|
||||
);
|
||||
productName = SampleApp;
|
||||
productReference = 0E6E96F72EB93F9C0030E0DA /* Demo.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
0E6E96EF2EB93F9C0030E0DA /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 2600;
|
||||
LastUpgradeCheck = 2600;
|
||||
TargetAttributes = {
|
||||
0E6E96F62EB93F9C0030E0DA = {
|
||||
CreatedOnToolsVersion = 26.0;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 0E6E96F22EB93F9C0030E0DA /* Build configuration list for PBXProject "Demo" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 0E6E96EE2EB93F9C0030E0DA;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
0E6E97312EB93FCE0030E0DA /* XCLocalSwiftPackageReference "../" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 0E6E96F82EB93F9C0030E0DA /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
0E6E96F62EB93F9C0030E0DA /* Demo */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
0E6E96F52EB93F9C0030E0DA /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0E56B79C2EBCE521004F79FC /* QuickLayoutShowcaseAssets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
0E6E96F32EB93F9C0030E0DA /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
0E6E97002EB93F9D0030E0DA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
0E6E97012EB93F9D0030E0DA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
0E6E97032EB93F9D0030E0DA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.meta.SampleApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
0E6E97042EB93F9D0030E0DA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.meta.SampleApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
0E6E96F22EB93F9C0030E0DA /* Build configuration list for PBXProject "Demo" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
0E6E97002EB93F9D0030E0DA /* Debug */,
|
||||
0E6E97012EB93F9D0030E0DA /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
0E6E97022EB93F9D0030E0DA /* Build configuration list for PBXNativeTarget "Demo" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
0E6E97032EB93F9D0030E0DA /* Debug */,
|
||||
0E6E97042EB93F9D0030E0DA /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
0E6E97312EB93FCE0030E0DA /* XCLocalSwiftPackageReference "../" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = ../;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
0E6E97332EB93FDF0030E0DA /* QuickLayout */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0E6E97312EB93FCE0030E0DA /* XCLocalSwiftPackageReference "../" */;
|
||||
productName = QuickLayout;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 0E6E96EF2EB93F9C0030E0DA /* Project object */;
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
|
||||
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.
|
||||
81
Package.swift
Normal file
81
Package.swift
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// swift-tools-version: 6.2
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import CompilerPluginSupport
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "QuickLayout",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.macOS(.v10_15),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "QuickLayout",
|
||||
targets: ["QuickLayout"]
|
||||
),
|
||||
.library(
|
||||
name: "QuickLayoutCore",
|
||||
targets: ["QuickLayoutCore"]
|
||||
),
|
||||
.library(
|
||||
name: "FastResultBuilder",
|
||||
targets: ["FastResultBuilder"]
|
||||
),
|
||||
.library(
|
||||
name: "QuickLayoutBridge",
|
||||
targets: ["QuickLayoutBridge"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-syntax.git", from: "602.0.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "QuickLayout",
|
||||
dependencies: [
|
||||
"QuickLayoutMacro",
|
||||
"QuickLayoutBridge",
|
||||
],
|
||||
path: "Sources/QuickLayout/QuickLayout",
|
||||
exclude: [
|
||||
"__showcase__/"
|
||||
]
|
||||
),
|
||||
.macro(
|
||||
name: "QuickLayoutMacro",
|
||||
dependencies: [
|
||||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
||||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
|
||||
],
|
||||
path: "Sources/QuickLayout/QuickLayoutMacro",
|
||||
),
|
||||
.target(
|
||||
name: "QuickLayoutCore",
|
||||
path: "Sources/QuickLayout/QuickLayoutCore",
|
||||
),
|
||||
.target(
|
||||
name: "FastResultBuilder",
|
||||
path: "Sources/FastResultBuilder/FastResultBuilder",
|
||||
exclude: [
|
||||
"__tests__/"
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "QuickLayoutBridge",
|
||||
dependencies: ["FastResultBuilder", "QuickLayoutCore"],
|
||||
path: "Sources/QuickLayout/QuickLayoutBridge",
|
||||
exclude: [
|
||||
"__server_snapshot_tests__",
|
||||
"__tests__",
|
||||
]
|
||||
),
|
||||
],
|
||||
)
|
||||
240
README.md
Normal file
240
README.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# QuickLayout
|
||||
|
||||
QuickLayout is a declarative layout library for iOS, designed to work seamlessly with UIKit. It is lightning-fast, incredibly simple to use, and offers a powerful set of modern layout primitives.
|
||||
|
||||
QuickLayout's API will feel natural to many iOS engineers. With a small and well tested codebase, it is production ready. QuickLayout has significant usage across many of Instagram's core features.
|
||||
|
||||
### Code Sample
|
||||
|
||||
```swift
|
||||
import QuickLayout
|
||||
|
||||
@QuickLayout
|
||||
class MyCellView: UIView {
|
||||
|
||||
let titleLabel = UILabel()
|
||||
let subtitleLabel = UILabel()
|
||||
|
||||
var body: Layout {
|
||||
HStack {
|
||||
VStack(alignment: leading) {
|
||||
titleLabel
|
||||
Spacer(4)
|
||||
subtitleLabel
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Above is a fully functional view. Subviews are managed automatically, layout is performed at the correct time, and sizeThatFits returns an accurate value.
|
||||
|
||||
### Features
|
||||
|
||||
- ⭐️ **Lightning-Fast**: Custom layout engine for blazing fast performance—no Auto Layout or Flexbox overhead.
|
||||
- 🧩 **Modern Layout Primitives**: Includes `HStack`, `VStack`, `ZStack`, `Grid`, and `Flow` as lightweight, pure Swift structs that don’t add extra views.
|
||||
- 🚀 **Easy Integration**: Simple @QuickLayout macro for fast setup and automatic subview management.
|
||||
- 🧵 **Thread-Safe**: [With some caveats](https://facebookincubator.github.io/QuickLayout/concepts/thread-safety/), our API can be used concurrently and off the main thread.
|
||||
|
||||
### Superior Performance over Auto Layout
|
||||
|
||||
QuickLayout achieves lightning-fast performance by using a custom-built layout engine that completely avoids Auto Layout and Flexbox, eliminating the overhead of constraint solving and the risk of constraint errors. Instead, QuickLayout provides a declarative API with lightweight layout primitives implemented as pure Swift structs that do not add extra views to the view hierarchy. This results in lower memory usage and faster layout calculations.
|
||||
|
||||
In benchmarks, QuickLayout is up to 3× faster and 4× more memory efficient than UIStackViews built with Auto Layout. By eliminating constraint solving and extra container views, QuickLayout delivers consistently high performance and a smaller memory footprint, making it ideal for demanding app surfaces.
|
||||
|
||||
For a comprehensive overview of all features, usage examples, and best practices, check out the [**QuickLayout Handbook**](https://facebookincubator.github.io/QuickLayout/).
|
||||
|
||||
## Quickstart
|
||||
|
||||
QuickLayout is a declarative layout library built to be used with regular UIViews. It can coexist with manual layout methods, allowing for gradual adoption.
|
||||
|
||||
### Stacks
|
||||
|
||||
At the heart of QuickLayout are Stacks:
|
||||
- VStack layouts child elements vertically,
|
||||
- HStack positions child elements horizontally.
|
||||
|
||||
For example, to make the following layout (image below), you can use VStack with leading alignment and spacing of four points:
|
||||
|
||||
<img width="150" src='Sources/QuickLayout/docs/static/img/quickstart/img1.png' />
|
||||
|
||||
```swift
|
||||
let label1 = UILabel(text: "Kylee Lessie")
|
||||
let label2 = UILabel(text: "Developer")
|
||||
|
||||
var body: Layout {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
label1
|
||||
label2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can nest Stacks as many times as you wish. For instance, you can add an icon to the left of two labels by using HStack:
|
||||
|
||||
<img width="150" src='Sources/QuickLayout/docs/static/img/quickstart/img2.png' />
|
||||
|
||||
```swift
|
||||
let avatarView = UIImage(image: avatarIcon)
|
||||
let label1 = UILabel(text: "Kylee Lessie")
|
||||
let label2 = UILabel(text: "Developer")
|
||||
|
||||
var body: Layout {
|
||||
HStack(spacing: 8) {
|
||||
avatarView
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
label1
|
||||
label2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Alignment in Stacks
|
||||
|
||||
The alignment property in stacks does not position the stack within its parent; instead, it controls the alignment of the child elements inside the stack.
|
||||
|
||||
The alignment property of a VStack only applies to the horizontal alignment of the contained views. Similarly, the alignment property for an HStack only controls the vertical alignment.
|
||||
|
||||
<img width="800" src='Sources/QuickLayout/docs/static/img/quickstart/img3.png' />
|
||||
|
||||
### Flexible Spacer
|
||||
|
||||
A flexible Spacer is an adaptive element that expands as much as possible. For example, when placed within an HStack, a spacer expands horizontally as much as the stack allows, moving sibling views out of the way within the stack’s size limits.
|
||||
|
||||
<img width="400" src='Sources/QuickLayout/docs/static/img/quickstart/img4.png' />
|
||||
|
||||
```swift
|
||||
var body: Layout {
|
||||
HStack {
|
||||
view1
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<img width="400" src='Sources/QuickLayout/docs/static/img/quickstart/img5.png' />
|
||||
|
||||
```swift
|
||||
var body: Layout {
|
||||
HStack {
|
||||
view1
|
||||
Spacer()
|
||||
view2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<img width="400" src='Sources/QuickLayout/docs/static/img/quickstart/img6.png' />
|
||||
|
||||
```swift
|
||||
var body: Layout {
|
||||
HStack {
|
||||
Spacer()
|
||||
view1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fixed spacer and Spacing
|
||||
|
||||
<img width="400" src='Sources/QuickLayout/docs/static/img/quickstart/img7.png' />
|
||||
|
||||
A fixed space between views can be specified with a Spacer as in the snippet below:
|
||||
|
||||
|
||||
```swift
|
||||
var body: Layout {
|
||||
HStack {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
Spacer(8)
|
||||
view3
|
||||
}
|
||||
}
|
||||
```
|
||||
You can achieve the same fixed spacing between views using the Stack's spacing parameter:
|
||||
|
||||
```swift
|
||||
var body: Layout {
|
||||
HStack(spacing: 8) {
|
||||
view1
|
||||
view2
|
||||
view3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Padding
|
||||
|
||||
A padding can be added to any view or layout element:
|
||||
|
||||
```swift
|
||||
var body: Layout {
|
||||
HStack(spacing: 8) {
|
||||
view1
|
||||
view2
|
||||
view3
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
```
|
||||
|
||||
### Frame
|
||||
|
||||
The frame does not directly change the size of the target view, but it acts like a "picture frame" by suggesting the child its size and positioning it inside its bounds. For example, if the UIImageView in the following layout has an icon that is 10x10 points in size, the icon will be surrounded by 45 points of empty space on each side. However, if the icon has a size of 90x90 points, it will be surrounded only by 5 points of empty space on each side.
|
||||
|
||||
```swift
|
||||
var body: Layout {
|
||||
imageView
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
```
|
||||
|
||||
To make UIImageView acquire the size of the frame, it needs to be wrapped into resizable modifier:
|
||||
|
||||
```swift
|
||||
var body: Layout {
|
||||
imageView
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
For full documentation, visit the [**QuickLayout Handbook**](https://facebookincubator.github.io/QuickLayout/).
|
||||
|
||||
## Installation
|
||||
Use Swift Package Manager. Link and import QuickLayout target.
|
||||
|
||||
## Credits
|
||||
|
||||
- [Constantine Fry](https://github.com/constantine-fry) — Concept, architecture, layout and API design
|
||||
- [Jordan Smith](https://github.com/jordansmithnz) — @QuickLayout macro and API design, body API and automatic subview management
|
||||
- [Fabio Milano](https://github.com/fabiomassimo) — Demo showcase, BUCK support
|
||||
- [Jordan Smith](https://github.com/jordansmithnz) & [Fabio Milano](https://github.com/fabiomassimo) — Instagram adoption
|
||||
- [Andrey Mishanin](https://github.com/Andrey-Mishanin) & [Constantine Fry](https://github.com/constantine-fry) — Stack Layout
|
||||
- [Constantine Fry](https://github.com/constantine-fry) & [Saadh Zahid](https://github.com/saadhzahid) — Flow and Grid Layout
|
||||
- [Jordan Smith](https://github.com/jordansmithnz) — Custom alignment and alignment guides
|
||||
- [Thong Nguyen](https://github.com/tumtumtum) — Fast Result Builder
|
||||
|
||||
## Special Thanks
|
||||
|
||||
To **Scott James Remnant** of [netsplit.com](https://netsplit.com/swiftui/) and **Javier** of [SwiftUI Labs](https://swiftui-lab.com/) for their research and articles on SwiftUI.
|
||||
|
||||
To **Chris Eidhof**, **Daniel Eggert**, and **Florian Kugler** of [objc.io](https://www.objc.io) for their continued work on exploring SwiftUI layout.
|
||||
|
||||
To **Luc Dion** for his work on [LayoutFrameworkBenchmark](https://github.com/layoutBox/LayoutFrameworkBenchmark).
|
||||
|
||||
To the early adopters - **Sash Zats**, **Cory Wilhite**, **Chaoshuai Lyu**, **Steven Liu**, **Erik Kunz**, **Min Kim**, and many others - for their feedback and early adoption efforts.
|
||||
|
||||
## License
|
||||
QuickLayout is MIT-licensed, as found in the LICENSE file.
|
||||
|
||||
## Legal
|
||||
|
||||
Copyright © Meta Platforms, Inc • <a href="https://opensource.fb.com/legal/terms">Terms of Use</a> • <a href="https://opensource.fb.com/legal/privacy">Privacy Policy</a>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/*
|
||||
* A resultBuilder that builds an array of the given generic type (the results).
|
||||
*/
|
||||
@resultBuilder
|
||||
public struct FastArrayBuilder<T> {
|
||||
public static func buildBlock(_ expressions: any FastExpression...) -> BlockExpression {
|
||||
BlockExpression(expressions: expressions)
|
||||
}
|
||||
|
||||
public static func buildBlock(_ expressions: [any FastExpression]) -> BlockExpression {
|
||||
BlockExpression(expressions: expressions)
|
||||
}
|
||||
|
||||
public static func buildExpression(_ value: T) -> ValueExpression<T> {
|
||||
ValueExpression<T>(value: value)
|
||||
}
|
||||
|
||||
public static func buildExpression(_ value: T?) -> any FastExpression {
|
||||
if let value {
|
||||
ValueExpression(value: value)
|
||||
} else {
|
||||
NothingExpression()
|
||||
}
|
||||
}
|
||||
|
||||
public static func buildExpression(_ expression: FastExpression) -> any FastExpression {
|
||||
expression
|
||||
}
|
||||
|
||||
public static func buildOptional(_ block: BlockExpression?) -> any FastExpression {
|
||||
block ?? NothingExpression()
|
||||
}
|
||||
|
||||
public static func buildEither(first block: BlockExpression) -> BlockExpression {
|
||||
block
|
||||
}
|
||||
|
||||
public static func buildEither(second block: BlockExpression) -> BlockExpression {
|
||||
block
|
||||
}
|
||||
|
||||
public static func buildArray(_ elements: [FastExpression]) -> ArrayExpression {
|
||||
ArrayExpression(elements: elements)
|
||||
}
|
||||
|
||||
public static func buildFinalResult(_ block: BlockExpression) -> [T] {
|
||||
ResultsExpressionVisitor<T>.getResults(block: block)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol FastExpression {
|
||||
func apply(visitor: inout some ExpressionVisitor)
|
||||
}
|
||||
|
||||
public struct ValueExpression<ValueType>: FastExpression {
|
||||
public let value: ValueType
|
||||
|
||||
public init(value: ValueType) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public func apply(visitor: inout some ExpressionVisitor) {
|
||||
visitor.visit(value: self)
|
||||
}
|
||||
}
|
||||
|
||||
public struct NothingExpression: FastExpression {
|
||||
public init() {}
|
||||
|
||||
public func apply(visitor: inout some ExpressionVisitor) {
|
||||
visitor.visit(nothing: self)
|
||||
}
|
||||
}
|
||||
|
||||
public struct BlockExpression: FastExpression {
|
||||
let expressions: [FastExpression]
|
||||
|
||||
public init(expressions: [FastExpression]) {
|
||||
self.expressions = expressions
|
||||
}
|
||||
|
||||
public func apply(visitor: inout some ExpressionVisitor) {
|
||||
visitor.visit(block: self)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ArrayExpression: FastExpression {
|
||||
let elements: [FastExpression]
|
||||
|
||||
public init(elements: [FastExpression]) {
|
||||
self.elements = elements
|
||||
}
|
||||
|
||||
public func apply(visitor: inout some ExpressionVisitor) {
|
||||
visitor.visit(array: self)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol ExpressionVisitor {
|
||||
mutating func visit<V>(value: ValueExpression<V>)
|
||||
mutating func visit(nothing: NothingExpression)
|
||||
mutating func visit(block: BlockExpression)
|
||||
mutating func visit(array: ArrayExpression)
|
||||
}
|
||||
|
||||
public struct ResultsExpressionVisitor<T>: ExpressionVisitor {
|
||||
var results: [T] = []
|
||||
|
||||
public mutating func visit<V>(value: ValueExpression<V>) {
|
||||
// Only interested in values that we can add to the results
|
||||
if let value = value.value as? T {
|
||||
results.append(value)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func visit(array: ArrayExpression) {
|
||||
for expression in array.elements {
|
||||
visit(any: expression)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func visit(nothing: NothingExpression) {
|
||||
}
|
||||
|
||||
public mutating func visit(any expression: FastExpression) {
|
||||
expression.apply(visitor: &self)
|
||||
}
|
||||
|
||||
public mutating func visit(block: BlockExpression) {
|
||||
for expression in block.expressions {
|
||||
visit(any: expression)
|
||||
}
|
||||
}
|
||||
|
||||
static func getResults(block: BlockExpression) -> [T] {
|
||||
var visitor = ResultsExpressionVisitor<T>()
|
||||
|
||||
visitor.results.reserveCapacity(block.expressions.count)
|
||||
visitor.visit(block: block)
|
||||
|
||||
return visitor.results
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FastResultBuilder
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@resultBuilder
|
||||
public struct StackArrayBuilder<T> {
|
||||
public static func buildBlock() -> [T] { [] }
|
||||
public static func buildExpression(_ expression: T) -> [T] { [expression] }
|
||||
public static func buildExpression(_ expression: T?) -> [T] { [expression].compactMap { $0 } }
|
||||
public static func buildExpression(_ expression: [T]) -> [T] { expression }
|
||||
public static func buildBlock(_ components: [T]...) -> [T] { components.flatMap { $0 } }
|
||||
public static func buildArray(_ components: [[T]]) -> [T] { components.flatMap { $0 } }
|
||||
public static func buildOptional(_ component: [T]?) -> [T] { component ?? [] }
|
||||
public static func buildEither(first component: [T]) -> [T] { component }
|
||||
public static func buildEither(second component: [T]) -> [T] { component }
|
||||
public static func buildLimitedAvailability(_ component: [T]) -> [T] { component }
|
||||
}
|
||||
|
||||
struct Foo {
|
||||
@FastArrayBuilder<Int> var values: [Int] {
|
||||
1
|
||||
2
|
||||
10 as Int?
|
||||
|
||||
for i in 1...10 {
|
||||
i
|
||||
}
|
||||
|
||||
if true {
|
||||
99
|
||||
}
|
||||
|
||||
if false {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Bar {
|
||||
@StackArrayBuilder<Int> var values: [Int] {
|
||||
1
|
||||
2
|
||||
10 as Int?
|
||||
|
||||
for i in 1...10 {
|
||||
i
|
||||
}
|
||||
|
||||
if true {
|
||||
99
|
||||
}
|
||||
|
||||
if false {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FastResultsBuilderTests: XCTestCase {
|
||||
func test1() {
|
||||
XCTAssertEqual(Foo().values, [1, 2, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 99, 1])
|
||||
}
|
||||
|
||||
func ignore_testPerf1() {
|
||||
self.measure {
|
||||
for _ in 1...100000 {
|
||||
XCTAssertEqual(Foo().values, [1, 2, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 99, 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ignore_testPerf2() {
|
||||
self.measure {
|
||||
for _ in 1...100000 {
|
||||
XCTAssertEqual(Bar().values, [1, 2, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 99, 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Sources/QuickLayout/QuickLayout/QuickLayout.swift
Normal file
8
Sources/QuickLayout/QuickLayout/QuickLayout.swift
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@_exported import QuickLayoutBridge
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
@QuickLayout
|
||||
final class HelloWorldView: UIView {
|
||||
|
||||
private let imageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.image = UIImage(systemName: "globe.americas")
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let titleLabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Hello World!"
|
||||
label.textColor = .label
|
||||
return label
|
||||
}()
|
||||
|
||||
private let subtitleLabel = {
|
||||
let label = UILabel()
|
||||
label.text = "This is a simple layout with QuickLayout"
|
||||
label.textColor = .secondaryLabel
|
||||
return label
|
||||
}()
|
||||
|
||||
var body: Layout {
|
||||
HStack {
|
||||
imageView
|
||||
Spacer(8)
|
||||
VStack(alignment: .leading) {
|
||||
titleLabel
|
||||
subtitleLabel
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
HelloWorldView()
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
@MainActor
|
||||
@QuickLayout
|
||||
final class StateManagementView: UIView {
|
||||
|
||||
private var count = 0
|
||||
private let label = UILabel()
|
||||
|
||||
private lazy var button = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle("Add One", for: .normal)
|
||||
button.addTarget(self, action: #selector(addCount), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var reset = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle("Reset", for: .normal)
|
||||
button.tintColor = .systemRed
|
||||
button.addTarget(self, action: #selector(resetCount), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
var body: Layout {
|
||||
VStack(spacing: 8) {
|
||||
if count > 0 {
|
||||
label
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
button
|
||||
reset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func addCount() {
|
||||
count += 1
|
||||
label.text = "Count: \(count)"
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
@objc private func resetCount() {
|
||||
count = 0
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
StateManagementView()
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
private let numberColors: [UIColor] = [.systemRed, .systemBlue, .systemGray, .systemPink, .systemTeal, .systemBrown, .systemOrange, .systemYellow, .systemGreen, .systemIndigo]
|
||||
|
||||
@MainActor
|
||||
@QuickLayout
|
||||
final class UpdatingWithAnimationView: UIView {
|
||||
|
||||
private let numberViews: [UIView] = (1...10).map {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .title1)
|
||||
label.text = "\($0)"
|
||||
label.textColor = .white
|
||||
label.layer.cornerRadius = 8
|
||||
label.layer.cornerCurve = .continuous
|
||||
label.layer.masksToBounds = true
|
||||
label.textAlignment = .center
|
||||
label.backgroundColor = numberColors[$0 - 1]
|
||||
return label
|
||||
}
|
||||
|
||||
private lazy var button: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle("Update with Animations", for: .normal)
|
||||
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
var numberViewIsActive = [Bool].init(repeating: false, count: 10)
|
||||
|
||||
var body: Layout {
|
||||
VStack {
|
||||
HStack(spacing: 8) {
|
||||
for (index, numberView) in numberViews.enumerated() {
|
||||
if numberViewIsActive[index] {
|
||||
numberView
|
||||
.expand(by: CGSize(width: 16, height: 8))
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(20)
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func buttonTapped() {
|
||||
numberViewIsActive = numberViewIsActive.map { _ in Bool.random() }
|
||||
setNeedsLayout()
|
||||
UIView.animate(withDuration: 0.5, delay: 0, options: [.beginFromCurrentState, .curveEaseInOut]) {
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
UpdatingWithAnimationView()
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
/// Declarative Layout in a UICollectionView based screen.
|
||||
/// **Key concepts**:
|
||||
/// - Animations
|
||||
/// - Dynamic Text support
|
||||
/// - RTL support for free (edit scheme: https://pxl.cl/5MGDW)
|
||||
@MainActor
|
||||
protocol BarCardViewCellDelegate: AnyObject {
|
||||
func didTapShareButton(_ cell: BarCardViewCell, bar: BarModel)
|
||||
}
|
||||
|
||||
@QuickLayout
|
||||
final class BarCardViewCell: UICollectionViewCell {
|
||||
static let reuseIdentifier = "BarCardViewCellReuseIdentifier"
|
||||
|
||||
private var expandDescription = false
|
||||
|
||||
private(set) var bar: BarModel?
|
||||
|
||||
weak var delegate: BarCardViewCellDelegate?
|
||||
|
||||
private let blurEffectView = {
|
||||
let blurEffect = UIBlurEffect(style: .light)
|
||||
let visualEffectView = UIVisualEffectView(effect: blurEffect)
|
||||
visualEffectView.layer.masksToBounds = true
|
||||
visualEffectView.layer.cornerRadius = 20
|
||||
return visualEffectView
|
||||
}()
|
||||
|
||||
private let name = {
|
||||
let label = UILabel()
|
||||
let boldTitle1Font = UIFont(descriptor: UIFont.preferredFont(forTextStyle: .title1).fontDescriptor.withSymbolicTraits(.traitBold) ?? UIFont.preferredFont(forTextStyle: .title1).fontDescriptor, size: 0)
|
||||
label.font = boldTitle1Font
|
||||
label.textColor = .white
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
return label
|
||||
}()
|
||||
|
||||
private let locationName = {
|
||||
let label = UILabel()
|
||||
label.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
label.textColor = .white
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var descriptionLabel = {
|
||||
let label = UILabel()
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.numberOfLines = 0
|
||||
label.textColor = .white
|
||||
label.font = UIFont.preferredFont(forTextStyle: .callout)
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapContentLabel))
|
||||
addGestureRecognizer(tapGesture)
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var shareButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
|
||||
button.tintColor = .white
|
||||
button.addTarget(self, action: #selector(didTapShareButton), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private let mediaContent = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
return imageView
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
/// Reset the internal state
|
||||
expandDescription = false
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
var body: Layout {
|
||||
ZStack(alignment: .bottom) {
|
||||
mediaContent.resizable()
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
name
|
||||
locationName
|
||||
Spacer(10)
|
||||
descriptionLabel
|
||||
.frame(maxHeight: expandDescription ? nil : 50)
|
||||
}
|
||||
Spacer()
|
||||
shareButton
|
||||
}
|
||||
.padding(15)
|
||||
.background { blurEffectView }
|
||||
.offset(y: -(window?.safeAreaInsets.bottom ?? 0))
|
||||
}
|
||||
.padding(.horizontal, 15)
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
func prepare(with item: BarModel) {
|
||||
bar = item
|
||||
mediaContent.image = item.coverImage
|
||||
name.text = item.name
|
||||
locationName.text = item.locationName
|
||||
descriptionLabel.text = item.description
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc func didTapContentLabel() {
|
||||
expandDescription.toggle()
|
||||
setNeedsLayout()
|
||||
descriptionLabel.setNeedsDisplay()
|
||||
UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.4, options: []) {
|
||||
self.shareButton.alpha = self.expandDescription ? 1.0 : 0.0
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func didTapShareButton() {
|
||||
guard let bar else { return }
|
||||
delegate?.didTapShareButton(self, bar: bar)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
BarsListViewController()
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
|
||||
final class BarsListViewController: UIViewController {
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Int, BarModel>?
|
||||
|
||||
private lazy var collectionView = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.scrollDirection = .vertical
|
||||
layout.minimumLineSpacing = 0
|
||||
layout.minimumLineSpacing = 0
|
||||
// patternlint-disable-next-line ig-avoid-uiscreen-bounds-swift
|
||||
layout.itemSize = UIScreen.main.bounds.size
|
||||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.register(BarCardViewCell.self, forCellWithReuseIdentifier: BarCardViewCell.reuseIdentifier)
|
||||
|
||||
collectionView.alwaysBounceVertical = true
|
||||
collectionView.isPagingEnabled = true
|
||||
collectionView.contentInsetAdjustmentBehavior = .never
|
||||
collectionView.backgroundColor = .black
|
||||
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
configureDataSource()
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
self.view = collectionView
|
||||
}
|
||||
|
||||
func configureDataSource() {
|
||||
dataSource = UICollectionViewDiffableDataSource<Int, BarModel>(collectionView: collectionView) { (collectionView: UICollectionView, indexPath: IndexPath, item: BarModel) -> UICollectionViewCell? in
|
||||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BarCardViewCell.reuseIdentifier, for: indexPath) as? BarCardViewCell else {
|
||||
fatalError("Cannot create new cell")
|
||||
}
|
||||
cell.prepare(with: item)
|
||||
cell.delegate = self
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, BarModel>()
|
||||
snapshot.appendSections([1])
|
||||
snapshot.appendItems(BarsStore.entries)
|
||||
dataSource?.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension BarsListViewController: BarCardViewCellDelegate {
|
||||
func didTapShareButton(_ cell: BarCardViewCell, bar: BarModel) {
|
||||
let itemsToShare: [Any] = ["Check this out!", bar.shareURL]
|
||||
let activityVC = UIActivityViewController(activityItems: itemsToShare, applicationActivities: nil)
|
||||
activityVC.excludedActivityTypes = [.message, .airDrop]
|
||||
present(activityVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
|
||||
struct BarModel {
|
||||
let coverImage: UIImage
|
||||
let name: String
|
||||
let locationName: String
|
||||
let description: String
|
||||
let shareURL: URL
|
||||
}
|
||||
nonisolated extension BarModel: Hashable {}
|
||||
|
||||
// swiftlint:disable force_unwrapping
|
||||
actor BarsStore {
|
||||
static let entries = {
|
||||
return [
|
||||
BarModel(coverImage: UIImage(named: "1_handshake")!, name: "Handshake", locationName: "🇲🇽 Mexico City", description: "Hidden behind an enigmatic black door in Colonia Juarez, whose only sign is the number '13', lies Handshake Speakeasy. Copper arches frame the backbar and the long, marble counter sits in front like an altar to the cocktail. The devil is in the detail here: in the branded silver fixtures, the seriously smart mini-cocktail serves and the cooling system under the bar that ensures every glass stays frosty.", shareURL: URL(string: "https://www.instagram.com/handshake_bar")!),
|
||||
BarModel(coverImage: UIImage(named: "2_superbueno")!, name: "Superbueno", locationName: "🇺🇸 New York City", description: "Superbueno is a perfectly named bar. The bilingual explanation perfectly encapsulates the zeitgeist of this vibrant ode to Mexican-American culture. Industry veteran Ignacio ‘Nacho’ Jimenez collaborated with power publican Greg Boehm (Katana Kitten, Mace) to open an elevated cantina on New York City’s Lower East Side. Together, the power duo has created a festive environment buzzing with a diverse crowd from early afternoon opening through to late-night close. ", shareURL: URL(string: "https://www.instagram.com/superbuenonyc/")!),
|
||||
BarModel(coverImage: UIImage(named: "3_overstory")!, name: "Overstory", locationName: "🇺🇸 New York City", description: "A spectacular view of the New York City skyline serves as backdrop for Overstory, where the cocktail magic served up is just as memorable as the vistas beyond. Located on the 64th floor of historic Financial District building 70 Pine Street, the experience begins in the grand lobby of the art deco building, where guests are welcomed and escorted via elevator to the petite jewel box of a bar. There, an expansive wraparound terrace offers guests the chance to soak in the glittering energy of the city before (or after) settling in for cocktail service.", shareURL: URL(string: "https://www.instagram.com/overstory")!),
|
||||
BarModel(coverImage: UIImage(named: "4_martiny")!, name: "Martiny's", locationName: "🇺🇸 New York City", description: "Takuma Watanabe earned a devout following helming the bar program at New York City’s legendary Angel’s Share. After the beloved speakeasy closed, Watanabe opened Martiny’s, an homage to the drinking cultures of his Tokyo birthplace as well as his Manhattan home. Occupying a carriage house in Gramercy, once owned by artist Philip Martiny, the three-storey lounge is a temple to luxury.", shareURL: URL(string: "https://www.instagram.com/martinys_nyc")!),
|
||||
]
|
||||
}()
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
@QuickLayout
|
||||
final class AlignmentGuidesView: UIView {
|
||||
|
||||
private let groceriesTitleLabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Groceries"
|
||||
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
|
||||
return label
|
||||
}()
|
||||
|
||||
private let groceryItemLabels = ["Milk", "Eggs", "Bananas"].map {
|
||||
let label = UILabel()
|
||||
label.text = $0
|
||||
return label
|
||||
}
|
||||
|
||||
private let tasksTitleLabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Tasks"
|
||||
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
|
||||
return label
|
||||
}()
|
||||
|
||||
private let taskItemLabels = ["Laundry", "Cook dinner"].map {
|
||||
let label = UILabel()
|
||||
label.text = $0
|
||||
return label
|
||||
}
|
||||
|
||||
var body: Layout {
|
||||
///
|
||||
///Note: This is for demonstration purposes only. Prefer using .padding() modifier when the alignmentGuide returns fixed value.
|
||||
///
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
groceriesTitleLabel
|
||||
for item in groceryItemLabels {
|
||||
item.alignmentGuide(.leading) { _ in -10 }
|
||||
}
|
||||
Spacer(20)
|
||||
tasksTitleLabel
|
||||
for item in taskItemLabels {
|
||||
item.alignmentGuide(.leading) { _ in -10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
AlignmentGuidesView()
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
private extension VerticalAlignment {
|
||||
|
||||
struct FirstThirdAlignment: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
context.height / 3
|
||||
}
|
||||
}
|
||||
|
||||
static let firstThirdAlignment = VerticalAlignment(FirstThirdAlignment.self)
|
||||
}
|
||||
|
||||
@QuickLayout
|
||||
final class CustomAlignmentView: UIView {
|
||||
|
||||
private let colorViews = (0...8).map { index in
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemBlue
|
||||
return view
|
||||
}
|
||||
|
||||
private let labels = (0...8).map { index in
|
||||
let label = UILabel()
|
||||
label.text = "\(index)"
|
||||
label.textColor = .white
|
||||
label.font = .monospacedDigitSystemFont(ofSize: 18, weight: .medium)
|
||||
return label
|
||||
}
|
||||
|
||||
var body: Layout {
|
||||
HStack(alignment: .firstThirdAlignment, spacing: 2) {
|
||||
VStack(spacing: 2) {
|
||||
colorViews[0].overlay { labels[0] }
|
||||
colorViews[1].overlay { labels[1] }
|
||||
colorViews[2].overlay { labels[2] }
|
||||
}.frame(height: 140)
|
||||
VStack(spacing: 2) {
|
||||
colorViews[3].overlay { labels[3] }
|
||||
colorViews[4].overlay { labels[4] }
|
||||
colorViews[5].overlay { labels[5] }
|
||||
}.frame(height: 250)
|
||||
VStack(spacing: 2) {
|
||||
colorViews[6].overlay { labels[6] }
|
||||
colorViews[7].overlay { labels[7] }
|
||||
colorViews[8].overlay { labels[8] }
|
||||
}.frame(height: 180)
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
CustomAlignmentView()
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
private extension VerticalAlignment {
|
||||
|
||||
private struct EmojiTitleAlignment: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
context[VerticalAlignment.bottom]
|
||||
}
|
||||
}
|
||||
|
||||
static let emojiTitle = VerticalAlignment(
|
||||
EmojiTitleAlignment.self
|
||||
)
|
||||
}
|
||||
|
||||
@QuickLayout
|
||||
final class EmojiAlignmentView: UIView {
|
||||
|
||||
private let emojis: [EmojiDisplay]
|
||||
private let titleView = UILabel()
|
||||
|
||||
init() {
|
||||
self.titleView.text = "Odd one out?"
|
||||
self.titleView.font = .systemFont(ofSize: 17, weight: .bold)
|
||||
self.emojis = _models.map {
|
||||
let emojiView = UILabel()
|
||||
emojiView.text = $0.emojiString
|
||||
let titleView = UILabel()
|
||||
titleView.text = $0.emojiTitle
|
||||
let descriptionView: UILabel?
|
||||
if let description = $0.emojiDescription {
|
||||
descriptionView = UILabel()
|
||||
descriptionView?.text = description
|
||||
descriptionView?.textColor = .secondaryLabel
|
||||
descriptionView?.font = .systemFont(ofSize: 13)
|
||||
emojiView.font = .systemFont(ofSize: 40)
|
||||
} else {
|
||||
descriptionView = nil
|
||||
}
|
||||
return EmojiDisplay(emojiView: emojiView, emojiTitleView: titleView, emojiDescriptionView: descriptionView)
|
||||
}
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var body: Layout {
|
||||
VStack(spacing: 30) {
|
||||
titleView
|
||||
HStack(alignment: .emojiTitle, spacing: 16) {
|
||||
for emojiDisplay in emojis {
|
||||
VStack {
|
||||
emojiDisplay.emojiView
|
||||
emojiDisplay.emojiTitleView
|
||||
.alignmentGuide(.emojiTitle) { _ in 0 }
|
||||
emojiDisplay.emojiDescriptionView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Model {
|
||||
let emojiString: String
|
||||
let emojiTitle: String
|
||||
let emojiDescription: String?
|
||||
}
|
||||
|
||||
private struct EmojiDisplay {
|
||||
let emojiView: UIView
|
||||
let emojiTitleView: UIView
|
||||
let emojiDescriptionView: UIView?
|
||||
}
|
||||
|
||||
private let _models: [Model] = [
|
||||
.init(emojiString: "🍔", emojiTitle: "Burger", emojiDescription: nil),
|
||||
.init(emojiString: "🍇", emojiTitle: "Grape", emojiDescription: nil),
|
||||
.init(emojiString: "🏡", emojiTitle: "House", emojiDescription: "This one!"),
|
||||
.init(emojiString: "🍎", emojiTitle: "Apple", emojiDescription: nil),
|
||||
]
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
EmojiAlignmentView()
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
private extension VerticalAlignment {
|
||||
|
||||
private struct TitleCenterAlignment: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
context[VerticalAlignment.center]
|
||||
}
|
||||
}
|
||||
|
||||
static let titleCenterAlignment = VerticalAlignment(TitleCenterAlignment.self)
|
||||
}
|
||||
|
||||
@QuickLayout
|
||||
final class FirstLabelAlignmentView: UIView {
|
||||
|
||||
let iconView = UIImageView(image: UIImage(systemName: "face.smiling")!) // swiftlint:disable:this force_unwrapping
|
||||
let titleView = UILabel()
|
||||
let subtitleLabel = UILabel()
|
||||
|
||||
init() {
|
||||
self.titleView.text = "Mauris fringilla ligula felis, nec pharetra velit congue id. Aenean hendrerit arcu lorem, in tempor est posuere id."
|
||||
self.titleView.font = UIFont.preferredFont(forTextStyle: .headline)
|
||||
self.titleView.numberOfLines = 0
|
||||
|
||||
self.subtitleLabel.text = "Lorem ipsum dolor sit amet consectetur adipiscing elit"
|
||||
self.subtitleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
|
||||
self.subtitleLabel.numberOfLines = 0
|
||||
|
||||
self.iconView.layer.borderColor = UIColor.systemGray.cgColor
|
||||
self.iconView.layer.borderWidth = 1
|
||||
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var body: Layout {
|
||||
HStack(alignment: .titleCenterAlignment) {
|
||||
iconView
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.alignmentGuide(.titleCenterAlignment, computeValue: { d in d[.top] + d.height / 2 })
|
||||
Spacer(16)
|
||||
VStack(alignment: .leading) {
|
||||
titleView
|
||||
.alignmentGuide(.titleCenterAlignment, computeValue: { d in d[.top] + d.height / 2 })
|
||||
subtitleLabel
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
FirstLabelAlignmentView()
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
private extension VerticalAlignment {
|
||||
|
||||
private enum ArrowAlignment: AlignmentID {
|
||||
static func defaultValue(in d: ViewDimensions) -> CGFloat {
|
||||
/// The value here doesn't matter, as the ArrowAlignment is used just as identifier for alignmentGuides.
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
static let arrowAlignment = VerticalAlignment(ArrowAlignment.self)
|
||||
}
|
||||
|
||||
private extension HorizontalAlignment {
|
||||
|
||||
private enum ListAlignment: AlignmentID {
|
||||
static func defaultValue(in d: ViewDimensions) -> CGFloat {
|
||||
/// The value here doesn't matter, as the ListAlignment is used just as identifier for alignmentGuides.
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
static let listAlignment = HorizontalAlignment(ListAlignment.self)
|
||||
}
|
||||
|
||||
@QuickLayout
|
||||
final class FruitSelectorView: UIView {
|
||||
|
||||
private let iconView = UIImageView(image: UIImage(systemName: "arrow.right.circle.fill")!) // swiftlint:disable:this force_unwrapping
|
||||
private let labels: [UILabel]
|
||||
private var selectedLabel: UILabel
|
||||
|
||||
init() {
|
||||
let words = ["Mango", "Strawberries", "Pineapple", "Watermelon", "Orange", "Apple", "Blueberries"]
|
||||
|
||||
self.labels = words.map { word in
|
||||
let label = UILabel()
|
||||
label.text = word
|
||||
label.textAlignment = .center
|
||||
label.font = UIFont.preferredFont(forTextStyle: .title2)
|
||||
return label
|
||||
}
|
||||
|
||||
selectedLabel = labels[1]
|
||||
super.init(frame: .zero)
|
||||
self.labels.forEach { label in
|
||||
let gr = UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))
|
||||
label.isUserInteractionEnabled = true
|
||||
label.addGestureRecognizer(gr)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func didTap(_ sender: UITapGestureRecognizer) {
|
||||
let label = sender.view
|
||||
if let label = self.labels.first(where: { $0 === label }) {
|
||||
selectedLabel = label
|
||||
UIView.animate(withDuration: 0.33, delay: 0.0, options: .curveEaseInOut) {
|
||||
self.setNeedsLayout()
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var body: Layout {
|
||||
|
||||
/// Using .arrowAlignment here makes HStack align the arrow icon with the center of the selected label.
|
||||
HStack(alignment: .arrowAlignment, spacing: 10) {
|
||||
|
||||
iconView
|
||||
.alignmentGuide(.arrowAlignment) { d in
|
||||
/// Define .arrowAlignment as the center of the icon.
|
||||
d[VerticalAlignment.center]
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(labels) { label in
|
||||
if label === selectedLabel {
|
||||
label
|
||||
.alignmentGuide(.arrowAlignment) { d in
|
||||
/// Define .arrowAlignment as the center the selected label.
|
||||
d[VerticalAlignment.center]
|
||||
}
|
||||
} else {
|
||||
label
|
||||
}
|
||||
}
|
||||
}
|
||||
.alignmentGuide(.listAlignment) { d in
|
||||
/// Define .listAlignment as the horizontal center of the list.
|
||||
/// This is to exclude the size of the iconView.
|
||||
d[HorizontalAlignment.center]
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.alignmentGuide(HorizontalAlignment.center) { d in
|
||||
/// Override HorizontalAlignment.center with .listAlignment
|
||||
/// from the child VStack. This is to make the view container view align the list of labels in the center without including the size of the iconView.
|
||||
d[.listAlignment]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
FruitSelectorView()
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
@QuickLayout
|
||||
final class AdvanceSizingContainerView: UIView {
|
||||
|
||||
private let exampleView = AdvanceSizingView(with: "This is an example view", image: UIImage(systemName: "lightbulb"))
|
||||
private let sizeInfoOne = UILabel()
|
||||
private let sizeInfoTwo = UILabel()
|
||||
|
||||
init() {
|
||||
let firstExampleSize = AdvanceSizingView.sizeThatFits(UIScreen.main.bounds.size, with: "Hi", image: UIImage(systemName: "hand.wave"))
|
||||
sizeInfoOne.numberOfLines = 0
|
||||
sizeInfoOne.textAlignment = .center
|
||||
sizeInfoOne.text = "Size with title 'Hi' and waving image. Width: \(firstExampleSize.width), height: \(firstExampleSize.height)"
|
||||
|
||||
let secondExampleSize = AdvanceSizingView.sizeThatFits(UIScreen.main.bounds.size, with: "Hello World", image: nil)
|
||||
sizeInfoTwo.numberOfLines = 0
|
||||
sizeInfoTwo.textAlignment = .center
|
||||
sizeInfoTwo.text = "Size with title 'Hello World' and no image. Width: \(secondExampleSize.width), height: \(secondExampleSize.height)"
|
||||
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
@MainActor required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var body: Layout {
|
||||
VStack(spacing: 12) {
|
||||
exampleView
|
||||
Spacer(20)
|
||||
sizeInfoOne
|
||||
sizeInfoTwo
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
AdvanceSizingContainerView()
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
@QuickLayout
|
||||
final class AdvanceSizingView: UIView {
|
||||
|
||||
private let label = UILabel()
|
||||
private let imageView = UIImageView()
|
||||
private let separator = UIView()
|
||||
|
||||
init(with text: String, image: UIImage?) {
|
||||
label.text = text
|
||||
imageView.image = image
|
||||
separator.backgroundColor = .gray
|
||||
separator.layer.cornerRadius = 1
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
override convenience init(frame: CGRect) {
|
||||
self.init(with: "This is an example view", image: UIImage(systemName: "lightbulb"))
|
||||
}
|
||||
|
||||
@MainActor required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var body: Layout {
|
||||
VStack(spacing: 5) {
|
||||
HStack(spacing: 8) {
|
||||
label
|
||||
imageView
|
||||
}
|
||||
separator
|
||||
.frame(height: 2)
|
||||
.frame(maxWidth: 220)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Advance Sizing
|
||||
|
||||
static func sizeThatFits(_ size: CGSize, with text: String, image: UIImage?) -> CGSize {
|
||||
VStack(spacing: 5) {
|
||||
HStack(spacing: 8) {
|
||||
UILabel.proxy(for: text)
|
||||
UIImageView.proxy(for: image)
|
||||
}
|
||||
ViewProxy(width: 220, height: 2)
|
||||
}
|
||||
.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
AdvanceSizingView(with: "This is an example view", image: UIImage(systemName: "lightbulb"))
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
@MainActor
|
||||
@QuickLayout
|
||||
final class LazyInstantiationView: UIView {
|
||||
|
||||
private let firstLabel = LazyView {
|
||||
let label = UILabel()
|
||||
label.text = "One"
|
||||
label.textColor = .systemOrange
|
||||
label.font = .systemFont(ofSize: 20, weight: .bold)
|
||||
return label
|
||||
}
|
||||
|
||||
private let secondLabel = LazyView {
|
||||
let label = UILabel()
|
||||
label.text = "Two"
|
||||
label.textColor = .systemGreen
|
||||
label.font = .systemFont(ofSize: 20, weight: .bold)
|
||||
return label
|
||||
}
|
||||
|
||||
private let thirdLabel = LazyView {
|
||||
let label = UILabel()
|
||||
label.text = "Three"
|
||||
label.textColor = .systemIndigo
|
||||
label.font = .systemFont(ofSize: 20, weight: .bold)
|
||||
return label
|
||||
}
|
||||
|
||||
private let addLabelButton = LazyView {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle("Add", for: .normal)
|
||||
return button
|
||||
}
|
||||
|
||||
private let removeLabelButton = LazyView {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitleColor(.systemRed, for: .normal)
|
||||
button.setTitleColor(.systemGray, for: .disabled)
|
||||
button.setTitle("Remove", for: .normal)
|
||||
return button
|
||||
}
|
||||
|
||||
private let descriptionLabel = LazyView {
|
||||
let label = UILabel()
|
||||
label.numberOfLines = 0
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}
|
||||
|
||||
private var numberOfLabels = 0
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
addLabelButton.loadIfNeeded().addTarget(self, action: #selector(addLabel), for: .touchUpInside)
|
||||
removeLabelButton.loadIfNeeded().addTarget(self, action: #selector(removeLabel), for: .touchUpInside)
|
||||
updateState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
var body: Layout {
|
||||
VStack(spacing: 16) {
|
||||
HStack(spacing: 8) {
|
||||
if numberOfLabels > 0 {
|
||||
firstLabel
|
||||
}
|
||||
if numberOfLabels > 1 {
|
||||
secondLabel
|
||||
}
|
||||
if numberOfLabels > 2 {
|
||||
thirdLabel
|
||||
}
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
removeLabelButton
|
||||
addLabelButton
|
||||
}
|
||||
descriptionLabel
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func addLabel() {
|
||||
updateNumberOfLabels(to: numberOfLabels + 1)
|
||||
}
|
||||
|
||||
@objc private func removeLabel() {
|
||||
updateNumberOfLabels(to: numberOfLabels - 1)
|
||||
}
|
||||
|
||||
private func updateNumberOfLabels(to value: Int) {
|
||||
numberOfLabels = value
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
updateState()
|
||||
}
|
||||
|
||||
private func updateState() {
|
||||
addLabelButton.loadIfNeeded().isEnabled = numberOfLabels < 3
|
||||
removeLabelButton.loadIfNeeded().isEnabled = numberOfLabels > 0
|
||||
let description = """
|
||||
First loaded: \(firstLabel.isLoaded ? "✅" : "❌")
|
||||
Second loaded: \(secondLabel.isLoaded ? "✅" : "❌")
|
||||
Third loaded: \(thirdLabel.isLoaded ? "✅" : "❌")
|
||||
"""
|
||||
let attribtuedString = NSMutableAttributedString(string: description)
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.lineSpacing = 12
|
||||
attribtuedString.addAttribute(.paragraphStyle, value: style, range: .init(location: 0, length: description.count))
|
||||
descriptionLabel.loadIfNeeded().attributedText = attribtuedString
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
LazyInstantiationView()
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
@MainActor
|
||||
@QuickLayout
|
||||
final class MobileConfigMigrationView: UIView {
|
||||
|
||||
private var isBodyEnabledFeatureFlag = false
|
||||
|
||||
private lazy var bodyEnabledLabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Body is enabled ✅"
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var explainerLabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Enable body by toggling the switch below."
|
||||
label.numberOfLines = 0
|
||||
label.textColor = .secondaryLabel
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var bodyEnabledButton = {
|
||||
let bodySwitch = UISwitch()
|
||||
bodySwitch.addTarget(self, action: #selector(self.switchToggled), for: .valueChanged)
|
||||
bodySwitch.sizeToFit()
|
||||
return bodySwitch
|
||||
}()
|
||||
|
||||
var body: Layout {
|
||||
VStack {
|
||||
bodyEnabledLabel
|
||||
Spacer(10)
|
||||
explainerLabel
|
||||
Spacer(10)
|
||||
bodyEnabledButton
|
||||
}
|
||||
}
|
||||
|
||||
override var isBodyEnabled: Bool {
|
||||
isBodyEnabledFeatureFlag
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
if isBodyEnabled {
|
||||
return body.sizeThatFits(size)
|
||||
} else {
|
||||
// Calculate size in the imperative way.
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if !isBodyEnabled {
|
||||
imperativeLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private func imperativeLayout() {
|
||||
addSubview(explainerLabel)
|
||||
addSubview(bodyEnabledButton)
|
||||
let labelSize = explainerLabel.sizeThatFits(self.bounds.size)
|
||||
explainerLabel.bounds = CGRect(origin: .zero, size: labelSize)
|
||||
explainerLabel.center = CGPoint(x: bounds.midX, y: bounds.midY - 4)
|
||||
bodyEnabledButton.center = CGPoint(x: bounds.midX, y: explainerLabel.frame.maxY + 10 + bodyEnabledButton.bounds.height / 2)
|
||||
}
|
||||
|
||||
@objc private func switchToggled() {
|
||||
isBodyEnabledFeatureFlag.toggle()
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
MobileConfigMigrationView()
|
||||
}
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
|
||||
/// Interactive Grid Layout Example
|
||||
/// **Key concepts**:
|
||||
/// - Grid and GridRow layouts
|
||||
/// - Dynamic spacing controls with sliders
|
||||
/// - Font size scaling with real-time updates
|
||||
/// - Real-time layout updates
|
||||
/// - Interactive UI elements
|
||||
@QuickLayout
|
||||
final class GridLayoutExampleView: UIView {
|
||||
|
||||
private var horizontalSpacing: CGFloat = 8
|
||||
private var verticalSpacing: CGFloat = 8
|
||||
private var fontScale: CGFloat = 1.0
|
||||
|
||||
private lazy var slider1: UISlider = {
|
||||
let slider = UISlider()
|
||||
slider.minimumValue = 0
|
||||
slider.maximumValue = 40
|
||||
slider.value = Float(horizontalSpacing)
|
||||
slider.isContinuous = true
|
||||
slider.minimumTrackTintColor = UIColor(red: 0.2, green: 0.6, blue: 1.0, alpha: 1.0)
|
||||
slider.maximumTrackTintColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0)
|
||||
slider.thumbTintColor = UIColor(red: 0.1, green: 0.4, blue: 0.8, alpha: 1.0)
|
||||
slider.addTarget(self, action: #selector(horizontalSliderChanged), for: .valueChanged)
|
||||
return slider
|
||||
}()
|
||||
|
||||
private lazy var slider2: UISlider = {
|
||||
let slider = UISlider()
|
||||
slider.minimumValue = 0
|
||||
slider.maximumValue = 40
|
||||
slider.value = Float(verticalSpacing)
|
||||
slider.isContinuous = true
|
||||
slider.minimumTrackTintColor = UIColor(red: 0.2, green: 0.6, blue: 1.0, alpha: 1.0)
|
||||
slider.maximumTrackTintColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0)
|
||||
slider.thumbTintColor = UIColor(red: 0.1, green: 0.4, blue: 0.8, alpha: 1.0)
|
||||
slider.addTarget(self, action: #selector(verticalSliderChanged), for: .valueChanged)
|
||||
return slider
|
||||
}()
|
||||
|
||||
private lazy var fontSizeSlider: UISlider = {
|
||||
let slider = UISlider()
|
||||
slider.minimumValue = 0.5
|
||||
slider.maximumValue = 2.0
|
||||
slider.value = Float(fontScale)
|
||||
slider.isContinuous = true
|
||||
slider.minimumTrackTintColor = UIColor(red: 0.2, green: 0.6, blue: 1.0, alpha: 1.0)
|
||||
slider.maximumTrackTintColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0)
|
||||
slider.thumbTintColor = UIColor(red: 0.1, green: 0.4, blue: 0.8, alpha: 1.0)
|
||||
slider.addTarget(self, action: #selector(fontSizeSliderChanged), for: .valueChanged)
|
||||
return slider
|
||||
}()
|
||||
|
||||
// Header labels
|
||||
private let item: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Item"
|
||||
label.font = .boldSystemFont(ofSize: 18)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.3, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let itemDescription: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Description"
|
||||
label.font = .boldSystemFont(ofSize: 18)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.3, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let quantity: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Quantity"
|
||||
label.font = .boldSystemFont(ofSize: 18)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.3, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let price: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Price"
|
||||
label.font = .boldSystemFont(ofSize: 18)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.3, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
// Data rows
|
||||
private let orange: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "🍊 Orange"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = UIColor(red: 0.3, green: 0.3, blue: 0.3, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let orangeDescription: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Sweet and tangy orange"
|
||||
label.font = .systemFont(ofSize: 15)
|
||||
label.textColor = UIColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let orangeQuantity: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Qty: 4"
|
||||
label.font = .systemFont(ofSize: 15)
|
||||
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let orangePrice: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "£1.30"
|
||||
label.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.6, blue: 0.2, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let apple: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "🍎 Apple"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = UIColor(red: 0.3, green: 0.3, blue: 0.3, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let appleDescription: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Juicy green apple"
|
||||
label.font = .systemFont(ofSize: 15)
|
||||
label.textColor = UIColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let appleQuantity: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Qty: 4"
|
||||
label.font = .systemFont(ofSize: 15)
|
||||
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let applePrice: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "£2.05"
|
||||
label.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.6, blue: 0.2, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let banana: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "🍌 Banana"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = UIColor(red: 0.3, green: 0.3, blue: 0.3, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let bananaDescription: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Fresh yellow bananas"
|
||||
label.font = .systemFont(ofSize: 15)
|
||||
label.textColor = UIColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let bananaQuantity: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Qty: 4"
|
||||
label.font = .systemFont(ofSize: 15)
|
||||
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let bananaPrice: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "£1.50"
|
||||
label.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.6, blue: 0.2, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private let totalPrice: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Total: £5.85"
|
||||
label.font = .boldSystemFont(ofSize: 18)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.3, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
// Control labels
|
||||
private var horizontalSpacingLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "↔️ Horizontal Spacing: 8"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.3, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private var verticalSpacingLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "↕️ Vertical Spacing: 8"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.3, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private var fontSizeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "🔤 Font Size: 100%"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = UIColor(red: 0.2, green: 0.3, blue: 0.5, alpha: 1.0)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = UIColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.0)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func horizontalSliderChanged(_ sender: UISlider) {
|
||||
horizontalSpacing = CGFloat(sender.value)
|
||||
horizontalSpacingLabel.text = "↔️ Horizontal Spacing: \(Int(horizontalSpacing))"
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
@objc private func verticalSliderChanged(_ sender: UISlider) {
|
||||
verticalSpacing = CGFloat(sender.value)
|
||||
verticalSpacingLabel.text = "↕️ Vertical Spacing: \(Int(verticalSpacing))"
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
@objc private func fontSizeSliderChanged(_ sender: UISlider) {
|
||||
fontScale = CGFloat(sender.value)
|
||||
fontSizeLabel.text = "🔤 Font Size: \(Int(fontScale * 100))%"
|
||||
updateAllFontSizes()
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
private func updateAllFontSizes() {
|
||||
// Header labels
|
||||
item.font = .boldSystemFont(ofSize: 18 * fontScale)
|
||||
itemDescription.font = .boldSystemFont(ofSize: 18 * fontScale)
|
||||
quantity.font = .boldSystemFont(ofSize: 18 * fontScale)
|
||||
price.font = .boldSystemFont(ofSize: 18 * fontScale)
|
||||
|
||||
// Data rows
|
||||
orange.font = .systemFont(ofSize: 16 * fontScale, weight: .medium)
|
||||
orangeDescription.font = .systemFont(ofSize: 15 * fontScale)
|
||||
orangeQuantity.font = .systemFont(ofSize: 15 * fontScale)
|
||||
orangePrice.font = .systemFont(ofSize: 16 * fontScale, weight: .semibold)
|
||||
|
||||
apple.font = .systemFont(ofSize: 16 * fontScale, weight: .medium)
|
||||
appleDescription.font = .systemFont(ofSize: 15 * fontScale)
|
||||
appleQuantity.font = .systemFont(ofSize: 15 * fontScale)
|
||||
applePrice.font = .systemFont(ofSize: 16 * fontScale, weight: .semibold)
|
||||
|
||||
banana.font = .systemFont(ofSize: 16 * fontScale, weight: .medium)
|
||||
bananaDescription.font = .systemFont(ofSize: 15 * fontScale)
|
||||
bananaQuantity.font = .systemFont(ofSize: 15 * fontScale)
|
||||
bananaPrice.font = .systemFont(ofSize: 16 * fontScale, weight: .semibold)
|
||||
|
||||
totalPrice.font = .boldSystemFont(ofSize: 20 * fontScale)
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
var body: Layout {
|
||||
VStack {
|
||||
|
||||
Spacer()
|
||||
|
||||
Grid(
|
||||
alignment: .leading,
|
||||
horizontalSpacing: horizontalSpacing,
|
||||
verticalSpacing: verticalSpacing
|
||||
) {
|
||||
GridRow {
|
||||
item
|
||||
itemDescription
|
||||
quantity
|
||||
price
|
||||
}
|
||||
|
||||
GridRow {
|
||||
orange
|
||||
orangeDescription
|
||||
orangeQuantity
|
||||
orangePrice
|
||||
}
|
||||
|
||||
GridRow {
|
||||
apple
|
||||
appleDescription
|
||||
appleQuantity
|
||||
applePrice
|
||||
}
|
||||
|
||||
GridRow {
|
||||
banana
|
||||
bananaDescription
|
||||
bananaQuantity
|
||||
bananaPrice
|
||||
}
|
||||
|
||||
GridRow {
|
||||
totalPrice
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.frame(width: 400)
|
||||
|
||||
Spacer()
|
||||
|
||||
Grid(alignment: .leading) {
|
||||
|
||||
GridRow {
|
||||
horizontalSpacingLabel
|
||||
.layoutPriority(1)
|
||||
slider1
|
||||
}
|
||||
|
||||
GridRow {
|
||||
verticalSpacingLabel
|
||||
slider2
|
||||
}
|
||||
|
||||
GridRow {
|
||||
fontSizeLabel
|
||||
fontSizeSlider
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
GridLayoutExampleView()
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayout
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct QuickLayoutShowcaseApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
// "No seriously, I need to debug my Declarative Layout view and i need an app that loads it!"
|
||||
//
|
||||
// Ok, ok, In that case use our ShowcaseViewRepresentable and pass to it your DeclarativeLayout View
|
||||
// and replace the ShowcaseApp() like this:
|
||||
//
|
||||
// ShowcaseViewRepresentable(MyView.self)
|
||||
//
|
||||
ShowcaseApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ShowcaseApp: View {
|
||||
private let showcases = [
|
||||
SCSection(
|
||||
"Hello World",
|
||||
[
|
||||
HelloWorldView.self
|
||||
]),
|
||||
SCSection(
|
||||
"State Management View",
|
||||
[
|
||||
StateManagementView.self
|
||||
]),
|
||||
|
||||
SCSection(
|
||||
"Alignment",
|
||||
[
|
||||
AlignmentGuidesView.self,
|
||||
CustomAlignmentView.self,
|
||||
EmojiAlignmentView.self,
|
||||
FirstLabelAlignmentView.self,
|
||||
FruitSelectorView.self,
|
||||
]),
|
||||
SCSection(
|
||||
"Advance Sizing",
|
||||
[
|
||||
AdvanceSizingContainerView.self,
|
||||
AdvanceSizingView.self,
|
||||
]),
|
||||
SCSection(
|
||||
"Lazy View",
|
||||
[
|
||||
LazyInstantiationView.self
|
||||
]),
|
||||
SCSection(
|
||||
"Mobile Config Migration View",
|
||||
[
|
||||
MobileConfigMigrationView.self
|
||||
]),
|
||||
SCSection(
|
||||
"Layout Containers",
|
||||
[
|
||||
GridLayoutExampleView.self
|
||||
]),
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("QuickLayout Demo")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
Text("Explore all major QuickLayout features in this interactive demo app or instantly preview your changes using Xcode Preview.")
|
||||
.font(.body)
|
||||
}
|
||||
.multilineTextAlignment(.leading)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
ForEach(showcases) { section in
|
||||
Section(section.title) {
|
||||
ForEach(section.showscases) { showcase in
|
||||
NavigationLink(destination: showcase.view()) {
|
||||
Text(showcase.label)
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ShowcaseViewRepresentable: UIViewRepresentable {
|
||||
|
||||
let view: UIView.Type
|
||||
|
||||
init(_ viewType: UIView.Type) {
|
||||
self.view = viewType
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> some UIView {
|
||||
view.init() as UIView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIViewType, context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private struct SCShowcase: Identifiable, Hashable {
|
||||
let id = UUID()
|
||||
|
||||
let viewType: UIView.Type
|
||||
let label: String
|
||||
|
||||
init(viewType: UIView.Type) {
|
||||
self.viewType = viewType
|
||||
self.label = String(describing: viewType).splittingCamelCase()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func view() -> some View {
|
||||
ShowcaseViewRepresentable(viewType)
|
||||
}
|
||||
|
||||
static func == (lhs: SCShowcase, rhs: SCShowcase) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
func splittingCamelCase() -> String {
|
||||
let stripped = self.hasSuffix("View") ? String(self.dropLast(4)) : self
|
||||
|
||||
// Split by uppercase letters
|
||||
let words = stripped.reduce("") { result, char in
|
||||
guard !result.isEmpty else { return String(char) }
|
||||
return char.isUppercase ? result + " " + String(char) : result + String(char)
|
||||
}
|
||||
|
||||
// Filter out single capital letters (abbreviations)
|
||||
let filtered = words.split(separator: " ")
|
||||
.filter { $0.count > 1 || $0.first?.isUppercase == false }
|
||||
.joined(separator: " ")
|
||||
|
||||
return String(filtered).trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SCSection: Identifiable, Hashable {
|
||||
let id = UUID()
|
||||
let showscases: [SCShowcase]
|
||||
let title: String
|
||||
|
||||
init(_ title: String, _ showcases: [UIView.Type]) {
|
||||
self.title = title
|
||||
self.showscases = showcases.map { SCShowcase(viewType: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preview code
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
ShowcaseApp()
|
||||
}
|
||||
59
Sources/QuickLayout/QuickLayoutBridge/LayoutBuilder.swift
Normal file
59
Sources/QuickLayout/QuickLayoutBridge/LayoutBuilder.swift
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import QuickLayoutCore
|
||||
import UIKit
|
||||
|
||||
// A custom parameter attribute that constructs layout from closures.
|
||||
@resultBuilder
|
||||
public struct LayoutBuilder {
|
||||
|
||||
public static func buildBlock(_ layout: Layout) -> Layout {
|
||||
layout
|
||||
}
|
||||
|
||||
public static func buildExpression(_ layout: Layout) -> Layout {
|
||||
layout
|
||||
}
|
||||
|
||||
public static func buildExpression(_ layout: Layout?) -> Layout {
|
||||
layout ?? EmptyLayout()
|
||||
}
|
||||
|
||||
public static func buildExpression(_ view: UIView) -> Layout {
|
||||
SingleElement(child: view)
|
||||
}
|
||||
|
||||
public static func buildExpression(_ view: UIView?) -> Layout {
|
||||
view.map { SingleElement(child: $0) } ?? EmptyLayout()
|
||||
}
|
||||
|
||||
public static func buildExpression(_ view: Element) -> Layout {
|
||||
SingleElement(child: view)
|
||||
}
|
||||
|
||||
public static func buildExpression(_ view: Element?) -> Layout {
|
||||
view.map { SingleElement(child: $0) } ?? EmptyLayout()
|
||||
}
|
||||
|
||||
public static func buildLimitedAvailability(_ layout: Layout) -> Layout {
|
||||
layout
|
||||
}
|
||||
|
||||
public static func buildEither(first: Layout) -> Layout {
|
||||
first
|
||||
}
|
||||
|
||||
public static func buildEither(second: Layout) -> Layout {
|
||||
second
|
||||
}
|
||||
|
||||
public static func buildOptional(_ layout: Layout?) -> Layout {
|
||||
layout ?? EmptyLayout()
|
||||
}
|
||||
}
|
||||
91
Sources/QuickLayout/QuickLayoutBridge/LazyView.swift
Normal file
91
Sources/QuickLayout/QuickLayoutBridge/LazyView.swift
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutCore
|
||||
|
||||
public struct LazyView<ViewType> {
|
||||
// MARK: - Setup
|
||||
private final class DeferredView {
|
||||
private enum State {
|
||||
case deferred(_ initializer: () -> ViewType)
|
||||
case resolved(value: ViewType)
|
||||
}
|
||||
|
||||
private var state: State
|
||||
|
||||
init(_ initializer: @escaping () -> ViewType) {
|
||||
self.state = .deferred(initializer)
|
||||
}
|
||||
|
||||
var resolved: ViewType {
|
||||
switch state {
|
||||
case .deferred(let initializer):
|
||||
let result = initializer()
|
||||
state = .resolved(value: result)
|
||||
return result
|
||||
case .resolved(let value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
var ifResolved: ViewType? {
|
||||
switch state {
|
||||
case .deferred:
|
||||
return nil
|
||||
case .resolved(let value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
var isResolved: Bool {
|
||||
switch state {
|
||||
case .deferred:
|
||||
return false
|
||||
case .resolved:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let view: DeferredView
|
||||
|
||||
// MARK: - API
|
||||
|
||||
public init(_ view: @escaping () -> ViewType) {
|
||||
self.view = DeferredView(view)
|
||||
}
|
||||
|
||||
public var ifLoaded: ViewType? {
|
||||
view.ifResolved
|
||||
}
|
||||
|
||||
public var isLoaded: Bool {
|
||||
view.isResolved
|
||||
}
|
||||
|
||||
@discardableResult public func loadIfNeeded() -> ViewType {
|
||||
view.resolved
|
||||
}
|
||||
}
|
||||
|
||||
extension LazyView: Element where ViewType: Element {
|
||||
public func quick_layoutThatFits(_ proposedSize: CGSize) -> LayoutNode {
|
||||
loadIfNeeded().quick_layoutThatFits(proposedSize)
|
||||
}
|
||||
|
||||
public func quick_flexibility(for axis: Axis) -> Flexibility {
|
||||
loadIfNeeded().quick_flexibility(for: axis)
|
||||
}
|
||||
|
||||
public func quick_layoutPriority() -> CGFloat {
|
||||
loadIfNeeded().quick_layoutPriority()
|
||||
}
|
||||
|
||||
public func quick_extractViewsIntoArray(_ views: inout [UIView]) {
|
||||
loadIfNeeded().quick_extractViewsIntoArray(&views)
|
||||
}
|
||||
}
|
||||
334
Sources/QuickLayout/QuickLayoutBridge/QuickLayout.swift
Normal file
334
Sources/QuickLayout/QuickLayoutBridge/QuickLayout.swift
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@_exported import FastResultBuilder
|
||||
@_exported import QuickLayoutCore
|
||||
import UIKit
|
||||
|
||||
/// Arranges its elements in a horizontal line. Spacing is added between each pair of elements.
|
||||
/// If spacing is 0, the elements are stacked without space between them.
|
||||
/// If there is a Spacer(x) or Spacer() between two views, the "spacing" parameter will be ignored for those two views.
|
||||
public func HStack(
|
||||
alignment: VerticalAlignment = .center,
|
||||
spacing: CGFloat = 0,
|
||||
@FastArrayBuilder<Element> children: () -> [Element]
|
||||
) -> StackElement {
|
||||
StackElement.horizontalStack(children: children(), spacing: spacing, alignment: alignment)
|
||||
}
|
||||
|
||||
/// Arranges its elements in a vertical line. Spacing is added between each pair of elements.
|
||||
/// If spacing is 0, the elements are stacked without space between them.
|
||||
/// If there is a Spacer(x) or Spacer() between two views, the "spacing" parameter will be ignored for those two views.
|
||||
public func VStack(
|
||||
alignment: HorizontalAlignment = .center,
|
||||
spacing: CGFloat = 0,
|
||||
@FastArrayBuilder<Element> children: () -> [Element]
|
||||
) -> StackElement {
|
||||
StackElement.verticalStack(children: children(), spacing: spacing, alignment: alignment)
|
||||
}
|
||||
|
||||
/// Overlays its elements, aligning them in both axes.
|
||||
/// ZStack proposes each child the parent's size, takes the size of the largest child, and then aligns each child within that size.
|
||||
/// See also .overlay modifier.
|
||||
public func ZStack(
|
||||
alignment: Alignment = .center,
|
||||
@FastArrayBuilder<Element> children: () -> [Element]
|
||||
) -> Element & Layout {
|
||||
ZStackElement(children: children(), alignment: alignment)
|
||||
}
|
||||
|
||||
/// Arranges its elements in a horizontal flow layout, wrapping to a new line when the current line exceeds the bounding space.
|
||||
public func HFlow(
|
||||
itemAlignment: VerticalAlignment = .center,
|
||||
lineAlignment: HorizontalAlignment = .center,
|
||||
itemSpacing: CGFloat = 0,
|
||||
lineSpacing: CGFloat = 0,
|
||||
@FastArrayBuilder<Element> children: () -> [Element]
|
||||
) -> Element & Layout {
|
||||
FlowElement(children: children(), mainAxis: .horizontal, itemSpacing: itemSpacing, lineSpacing: lineSpacing, itemAlignmentID: itemAlignment.alignmentID, lineAlignmentID: lineAlignment.alignmentID)
|
||||
}
|
||||
|
||||
/// Arranges its elements in a vertical flow layout, wrapping to a new column when the current column exceeds the bounding space.
|
||||
/// `itemSpacing` parameter controls the spacing between items on the same column.
|
||||
/// `lineSpacing` parameter controls the spacing between columns.
|
||||
/// `itemAlignment` parameter aligns items horizontally within their column.
|
||||
/// `lineAlignment` parameter aligns columns vertically within the overall layout.
|
||||
public func VFlow(
|
||||
itemAlignment: HorizontalAlignment = .center,
|
||||
lineAlignment: VerticalAlignment = .center,
|
||||
itemSpacing: CGFloat = 0,
|
||||
lineSpacing: CGFloat = 0,
|
||||
@FastArrayBuilder<Element> children: () -> [Element]
|
||||
) -> Element & Layout {
|
||||
FlowElement(children: children(), mainAxis: .vertical, itemSpacing: itemSpacing, lineSpacing: lineSpacing, itemAlignmentID: itemAlignment.alignmentID, lineAlignmentID: lineAlignment.alignmentID)
|
||||
}
|
||||
|
||||
/// Arranges its elements in a two-dimensional grid with rows and columns.
|
||||
/// `horizontalSpacing` parameter controls the spacing between columns.
|
||||
/// `verticalSpacing` parameter controls the spacing between rows.
|
||||
/// `alignment` parameter aligns elements within their cells.
|
||||
/// Each row can specify its own vertical alignment, and individual cells can override alignment using gridCellAnchor modifier.
|
||||
public func Grid(
|
||||
alignment: Alignment = .center,
|
||||
horizontalSpacing: CGFloat = 0,
|
||||
verticalSpacing: CGFloat = 0,
|
||||
@FastArrayBuilder<GridRowElement> rows: () -> [GridRowElement] = { [] },
|
||||
) -> Element & Layout {
|
||||
GridElement(rows: rows(), alignment: alignment, horizontalSpacing: horizontalSpacing, verticalSpacing: verticalSpacing)
|
||||
}
|
||||
|
||||
/// Represents a single row in a Grid layout.
|
||||
/// Each GridRow contains a horizontal collection of elements that form the cells in that row.
|
||||
/// `alignment` parameter controls the vertical alignment of elements within a specific row.
|
||||
public func GridRow(
|
||||
alignment: VerticalAlignment? = nil,
|
||||
@FastArrayBuilder<Element> children: () -> [Element]
|
||||
) -> GridRowElement {
|
||||
GridRowElement(children: children(), alignment: alignment)
|
||||
}
|
||||
|
||||
/// A flexible space that expands along the main axis of its containing stack.
|
||||
public func Spacer() -> Element {
|
||||
SpacerElement()
|
||||
}
|
||||
|
||||
/// A fixed length spacer. The length is applied only for the main axis of its containing stack.
|
||||
public func Spacer(_ length: CGFloat) -> Element {
|
||||
SpacerElement(length: length)
|
||||
}
|
||||
|
||||
public func Spacer(minLength: CGFloat) -> FastExpression {
|
||||
BlockExpression(expressions: [ValueExpression<Element>(value: Spacer()), ValueExpression<Element>(value: SpacerElement(length: minLength))])
|
||||
}
|
||||
|
||||
/// Null object pattern. Sizes to zero.
|
||||
public func EmptyLayout() -> Element & Layout {
|
||||
EmptyElement()
|
||||
}
|
||||
|
||||
public extension Element {
|
||||
|
||||
/// Constrains the elements’s dimensions to an aspect ratio specified by a CGSize.
|
||||
/// If this view is resizable, it uses aspectRatio as its own aspect ratio.
|
||||
func aspectRatio(_ ratio: CGSize, contentMode: ContentMode = .fit) -> Element & Layout {
|
||||
AspectRatioElement(child: self, aspectRatio: ratio.width / ratio.height, contentMode: contentMode)
|
||||
}
|
||||
|
||||
/// Adds the desired amount of space to the edges of this element.
|
||||
/// The leading and trailing margins are applied appropriately to the left or right margins based on the current layout direction.
|
||||
/// For example, the leading margin is applied to the right edge of the view in right-to-left layouts.
|
||||
func padding(_ edges: EdgeSet = .all, _ length: CGFloat) -> Element & Layout {
|
||||
PaddingElement(child: self, edges: edges, length: length)
|
||||
}
|
||||
|
||||
/// Adds the desired amount of space to the edges of this element.
|
||||
/// The leading and trailing margins are applied appropriately to the left or right margins based on the current layout direction.
|
||||
/// For example, the leading margin is applied to the right edge of the view in right-to-left layouts.
|
||||
func padding(_ insets: EdgeInsets) -> Element & Layout {
|
||||
PaddingElement(child: self, insets: insets)
|
||||
}
|
||||
|
||||
/// Adds the desired amount of space to all edges of this element.
|
||||
func padding(_ length: CGFloat) -> Element & Layout {
|
||||
PaddingElement(child: self, edges: .all, length: length)
|
||||
}
|
||||
|
||||
func padding(ignoringLayoutDirection insets: UIEdgeInsets) -> Element & Layout {
|
||||
PaddingElement(child: self, insets: insets)
|
||||
}
|
||||
|
||||
/// Offsets the child element by the specified amount.
|
||||
func offset(x: CGFloat = 0, y: CGFloat = 0) -> Element & Layout {
|
||||
OffsetElement(child: self, offset: CGPoint(x: x, y: y))
|
||||
}
|
||||
|
||||
/// Fixed Frame Modifier
|
||||
/// Positions the child element within an invisible container with the specified size.
|
||||
/// The modifier doesn’t change the size of its child element but acts more like a “picture frame” by letting the child be any size it wants and only positioning it inside its own bounds.
|
||||
/// If you want to "assign" a size to the child view, use the view.resizable().frame(width: l1, height: l2) modifier.
|
||||
/// If you omit a constraint (or pass nil), the frame will use the sizing behavior of its child element for that axis.
|
||||
/// When using this method, you must provide at least one size constraint.
|
||||
///
|
||||
/// Negative, nan, and infinite values are ignored.
|
||||
func frame(
|
||||
width: CGFloat? = nil,
|
||||
height: CGFloat? = nil,
|
||||
alignment: Alignment = .center
|
||||
) -> Element & Layout {
|
||||
FixedFrameElement(
|
||||
child: self,
|
||||
width: width,
|
||||
height: height,
|
||||
alignment: alignment
|
||||
)
|
||||
}
|
||||
|
||||
/// Flexible Frame Modifier
|
||||
/// Positions the child element inside an invisible, flexible container.
|
||||
/// The modifier doesn’t change the size of its child element but acts more like a “picture frame” by letting the child be any size it wants and only positioning it inside its own bounds.
|
||||
/// If you omit a constraint (or pass nil), the frame will use the sizing behavior of its child element for that axis.
|
||||
/// When using this method, you must provide at least one size constraint.
|
||||
func frame(
|
||||
minWidth: CGFloat? = nil,
|
||||
maxWidth: CGFloat? = nil,
|
||||
minHeight: CGFloat? = nil,
|
||||
maxHeight: CGFloat? = nil,
|
||||
alignment: Alignment = .center
|
||||
) -> Element & Layout {
|
||||
FlexibleFrameElement(
|
||||
child: self,
|
||||
minWidth: minWidth,
|
||||
maxWidth: maxWidth,
|
||||
minHeight: minHeight,
|
||||
maxHeight: maxHeight,
|
||||
alignment: alignment
|
||||
)
|
||||
}
|
||||
|
||||
/// Fixed Size Modifier
|
||||
/// Replaces the proposed size of the child with the infinity.
|
||||
func fixedSize(axis: AxisSet = [.horizontal, .vertical]) -> Element & Layout {
|
||||
FixedSizeElement(child: self, horizontal: axis.contains(.horizontal), vertical: axis.contains(.vertical))
|
||||
}
|
||||
|
||||
/// Sets the priority by which a parent V/HStack should allocate space to this child.
|
||||
/// A view's default priority is 0, which distributes available space evenly to all sibling views.
|
||||
/// Set a higher priority to measure the view first with a larger portion of available space.
|
||||
func layoutPriority(_ priority: CGFloat) -> Element {
|
||||
LayoutPriorityElement(child: self, layoutPriority: priority)
|
||||
}
|
||||
|
||||
/// Overrides the default layout direction.
|
||||
/// Doesn't affect the layout direction of child views.
|
||||
func layoutDirection(_ direction: LayoutDirection) -> Element & Layout {
|
||||
LayoutDirectionElement(child: self, layoutDirection: direction)
|
||||
}
|
||||
|
||||
/// Positions the content view within the frame of the target view.
|
||||
/// The modifier measures the target view, proposes the target view's size to the content view, and then aligns it within the frame of the target view.
|
||||
/// See also ZStack.
|
||||
func overlay(alignment: Alignment = .center, @LayoutBuilder content: () -> Element?) -> Element & Layout {
|
||||
LayeringElement(
|
||||
target: self,
|
||||
layer: content() ?? EmptyLayout(),
|
||||
type: .overlay,
|
||||
alignment: alignment
|
||||
)
|
||||
}
|
||||
|
||||
/// Positions the content view within the frame of the target view.
|
||||
/// The modifier measures the target view, proposes the target view's size to the content view, and then aligns it within the frame of the target view.
|
||||
/// See also ZStack.
|
||||
func background(alignment: Alignment = .center, @LayoutBuilder content: () -> Element?) -> Element & Layout {
|
||||
LayeringElement(
|
||||
target: self,
|
||||
layer: content() ?? EmptyLayout(),
|
||||
type: .background,
|
||||
alignment: alignment
|
||||
)
|
||||
}
|
||||
|
||||
/// When the specified horizontal alignment is being applied, the child
|
||||
/// element will be positioned according to the computed alignment value.
|
||||
func alignmentGuide(_ horizontalAlignment: HorizontalAlignment, computeValue: @escaping @Sendable (ViewDimensions) -> CGFloat) -> Element & Layout {
|
||||
AlignmentGuideElement(child: self, horizontalAlignment: horizontalAlignment, computeValue: computeValue)
|
||||
}
|
||||
|
||||
/// When the specified vertical alignment is being applied, the child
|
||||
/// element will be positioned according to the computed alignment value.
|
||||
func alignmentGuide(_ verticalAlignment: VerticalAlignment, computeValue: @escaping @Sendable (ViewDimensions) -> CGFloat) -> Element & Layout {
|
||||
AlignmentGuideElement(child: self, verticalAlignment: verticalAlignment, computeValue: computeValue)
|
||||
}
|
||||
|
||||
/// Grid cell Anchor Modifier
|
||||
/// Positions the child view within the cell of the grid.
|
||||
/// The modifier doesn’t change the size of its child directly but lets the child be
|
||||
/// any size it wants and only positioning it inside its own bounds.
|
||||
/// The modifier will always override the alignment set by the row or column.
|
||||
func gridCellAnchor(_ alignment: Alignment = .center) -> Element & Layout {
|
||||
GridCellAnchorElement(child: self, alignment: alignment)
|
||||
}
|
||||
|
||||
func gridCellAnchor(_ unitPoint: UnitPoint) -> Element & Layout {
|
||||
GridCellAnchorElement(child: self, unitPoint: unitPoint)
|
||||
}
|
||||
|
||||
/// Grid Column Alignment Modifier
|
||||
/// Overrides the default horizontal alignment of a grid column.
|
||||
/// The alignment will be applied to all elements in the same column as this element.
|
||||
func gridColumnAlignment(_ alignment: HorizontalAlignment = .center) -> Element & Layout {
|
||||
GridColumnAlignmentElement(child: self, alignment: alignment)
|
||||
}
|
||||
}
|
||||
|
||||
public extension LeafElement {
|
||||
|
||||
/// Makes the view ignore its intrinsic size (size returned by sizeThatFits) so it becomes fully flexible along both axes.
|
||||
/// If only one axis is specified, the target view intrinsic size is preserved along the other axis.
|
||||
func resizable(axis: AxisSet = [.horizontal, .vertical]) -> Element & Layout {
|
||||
ResizableElement(child: self, axis: axis)
|
||||
}
|
||||
|
||||
/// Measures the intrinsic size of the view and then adds the additional size.
|
||||
/// Nans and infinite values are ignored.
|
||||
func expand(by size: CGSize) -> Element & Layout {
|
||||
ExpandableElement(child: self, size: size)
|
||||
}
|
||||
}
|
||||
|
||||
extension StackElement {
|
||||
|
||||
/// Ideal Layout Modifier
|
||||
/// If enabled, the stack use the ideal layout algorithm to make children have equal size along the cross axis.
|
||||
public func idealLayout(_ enabled: Bool = true) -> StackElement {
|
||||
StackElement(
|
||||
children: children,
|
||||
mainAxis: mainAxis,
|
||||
spacing: spacing,
|
||||
alignmentID: alignmentID,
|
||||
idealLayout: enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
|
||||
/// Adds the given views as subviews of this view.
|
||||
public func addSubviews(@FastArrayBuilder<UIView> views: () -> [UIView]) {
|
||||
let viewList = views
|
||||
for v in viewList() {
|
||||
addSubview(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func ForEach(_ list: [Element]) -> FastExpression {
|
||||
BlockExpression(expressions: list.map { ValueExpression<Element>(value: $0) })
|
||||
}
|
||||
|
||||
public func ForEach(_ list: [Element], map: (Element) -> Element) -> FastExpression {
|
||||
BlockExpression(expressions: list.map { ValueExpression<Element>(value: map($0)) })
|
||||
}
|
||||
|
||||
public func ForEach(_ list: [UIView], map: (UIView) -> Element) -> FastExpression {
|
||||
BlockExpression(expressions: list.map { ValueExpression<Element>(value: map($0)) })
|
||||
}
|
||||
|
||||
public typealias Alignment = QuickLayoutCore.Alignment
|
||||
public typealias AlignmentID = QuickLayoutCore.AlignmentID
|
||||
public typealias Axis = QuickLayoutCore.Axis
|
||||
public typealias ContentMode = QuickLayoutCore.ContentMode
|
||||
public typealias EdgeInsets = QuickLayoutCore.EdgeInsets
|
||||
public typealias Element = QuickLayoutCore.Element
|
||||
public typealias LeafElement = QuickLayoutCore.LeafElement
|
||||
public typealias Flexibility = QuickLayoutCore.Flexibility
|
||||
public typealias HorizontalAlignment = QuickLayoutCore.HorizontalAlignment
|
||||
public typealias Layout = QuickLayoutCore.Layout
|
||||
public typealias LayoutDirection = QuickLayoutCore.LayoutDirection
|
||||
public typealias VerticalAlignment = QuickLayoutCore.VerticalAlignment
|
||||
public typealias ViewDimensions = QuickLayoutCore.ElementDimensions
|
||||
public typealias StackElement = QuickLayoutCore.StackElement
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
@QuickLayout is the recommended way to use QuickLayout. Adding it to a view will require that
|
||||
you define a `body` var that returns the layout of your view. @QuickLayout views do not require
|
||||
manual addition or removal of views, this is handled automatically (the body implicitly defines the
|
||||
views that should be present on the view hierarchy). Layout and sizing are also handled automatically;
|
||||
the only requirement is a body definition.
|
||||
*/
|
||||
@attached(member, names: named(willMove(toWindow:)), named(layoutSubviews), named(sizeThatFits), named(quick_flexibility(for:)))
|
||||
@attached(memberAttribute)
|
||||
@attached(extension, conformances: HasBody)
|
||||
public macro QuickLayout() = #externalMacro(module: "QuickLayoutMacro", type: "QuickLayout")
|
||||
|
||||
/**
|
||||
This macro is an implementation detail of @QuickLayout. It is not intended to be used directly.
|
||||
*/
|
||||
@attached(body)
|
||||
public macro _QuickLayoutInjection(_ value: String) = #externalMacro(module: "QuickLayoutMacro", type: "QuickLayoutInjection")
|
||||
10
Sources/QuickLayout/QuickLayoutBridge/README.md
Normal file
10
Sources/QuickLayout/QuickLayoutBridge/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# QuickLayout
|
||||
|
||||
QuickLayout is a declarative layout library, built to support the adaptive layout needs of UIKit-first iOS apps.
|
||||
This library adheres to the following core principles:
|
||||
|
||||
- Optional: It is not a replacement for UIKit, it is a simple abstraction allowing layout to be expressed declaratively.
|
||||
- Simple: The API surface is extremely minimal, with concepts that are very familiar for most iOS engineers.
|
||||
- Helpful: The minimal API we have helps solve real problems, and should make building UI feel delightful.
|
||||
|
||||
QuickLayout should stay true to these core values. It should not aim to solve problems outside the scope of UIKit layout, or add unnecessary features.
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
|
||||
private let appearanceAnimationKey = "BodyAppearanceCoordinator-appearance"
|
||||
private let disappearanceAnimationKey = "BodyAppearanceCoordinator-disappearance"
|
||||
private let disappearanceAnimationIDKey = "BodyAppearanceCoordinator-disappearance-id"
|
||||
|
||||
@MainActor
|
||||
final class BodyAppearanceCoordinator: NSObject, @preconcurrency CAAnimationDelegate {
|
||||
private var disappearanceAnimationID: Int = 0
|
||||
private var appearingViews: Set<UIView> = []
|
||||
private var activeViews: Set<UIView> = []
|
||||
private var appearingViewAnimationKeys: [UIView: Set<String>] = [:]
|
||||
private var disappearingViewAnimations: [Int: Weak<UIView>] = [:]
|
||||
private var disableViewAppearanceAnimations: Bool = false
|
||||
private weak var targetWindow: UIWindow?
|
||||
|
||||
// MARK: - API
|
||||
|
||||
func coordinateMove(to newWindow: UIWindow?) {
|
||||
targetWindow = newWindow
|
||||
if newWindow != nil {
|
||||
disableViewAppearanceAnimations = true
|
||||
// When moving window, we don't want to animate view appearance.
|
||||
// The animations applied by this class are intended for transitions
|
||||
// between onscreen view states. Applying animations as the body
|
||||
// itself moves on and offscreen is not intended; these animations
|
||||
// are external to the body and applying additional animations
|
||||
// would not be expected.
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard self?.targetWindow == newWindow else { return }
|
||||
self?.disableViewAppearanceAnimations = false
|
||||
}
|
||||
} else {
|
||||
disableViewAppearanceAnimations = false
|
||||
}
|
||||
}
|
||||
|
||||
func coordinateAppearance(of view: UIView, in superview: UIView, index: Int) {
|
||||
appearingViews.insert(view)
|
||||
activeViews.insert(view)
|
||||
superview.insertSubview(view, at: index)
|
||||
animateAppearanceIfNeeded(for: view)
|
||||
}
|
||||
|
||||
func coordinateDisappearance(of view: UIView) {
|
||||
appearingViews.remove(view)
|
||||
activeViews.remove(view)
|
||||
performDisappearanceWithAnimationIfNeeded(for: view)
|
||||
}
|
||||
|
||||
func beginViewAppearanceUpdates() {
|
||||
appearingViewAnimationKeys = appearingViews.reduce(into: [:]) { partialResult, view in
|
||||
partialResult[view] = view.animationKeys
|
||||
}
|
||||
}
|
||||
|
||||
func commitViewAppearanceUpdates() {
|
||||
for (view, existingAnimationKeys) in appearingViewAnimationKeys {
|
||||
let disallowedAnimationKeys = view.animationKeys.subtracting(existingAnimationKeys)
|
||||
view.removeAnimations(for: disallowedAnimationKeys)
|
||||
}
|
||||
appearingViews.removeAll()
|
||||
appearingViewAnimationKeys.removeAll()
|
||||
}
|
||||
|
||||
// MARK: - CAAnimationDelegate
|
||||
|
||||
func animationDidStop(_ animation: CAAnimation, finished flag: Bool) {
|
||||
guard let animationID = animation.value(forKey: disappearanceAnimationIDKey) as? Int else { return }
|
||||
defer { disappearingViewAnimations[animationID] = nil }
|
||||
guard let view = disappearingViewAnimations[animationID]?.value else { return }
|
||||
view.layer.removeAnimation(forKey: disappearanceAnimationKey)
|
||||
guard !activeViews.contains(view) else { return }
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func animateAppearanceIfNeeded(for view: UIView) {
|
||||
guard UIView.inheritedAnimationDuration > 0 && !disableViewAppearanceAnimations else { return }
|
||||
let animation = buildOpacityAnimation(initialOpacity: 0, targetOpacity: view.alpha)
|
||||
view.layer.add(animation, forKey: appearanceAnimationKey)
|
||||
}
|
||||
|
||||
private func performDisappearanceWithAnimationIfNeeded(for view: UIView) {
|
||||
guard UIView.inheritedAnimationDuration > 0 else {
|
||||
view.removeFromSuperview()
|
||||
return
|
||||
}
|
||||
let animation = buildOpacityAnimation(initialOpacity: view.alpha, targetOpacity: 0)
|
||||
animation.delegate = self
|
||||
animation.setValue(disappearanceAnimationID, forKey: disappearanceAnimationIDKey)
|
||||
disappearingViewAnimations[disappearanceAnimationID] = Weak(value: view)
|
||||
view.layer.add(animation, forKey: disappearanceAnimationKey)
|
||||
disappearanceAnimationID += 1
|
||||
}
|
||||
|
||||
private func buildOpacityAnimation(initialOpacity: CGFloat, targetOpacity: CGFloat) -> CABasicAnimation {
|
||||
let animation = CABasicAnimation(keyPath: "opacity")
|
||||
animation.duration = UIView.inheritedAnimationDuration
|
||||
animation.fillMode = .forwards
|
||||
animation.isRemovedOnCompletion = false
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
animation.fromValue = initialOpacity
|
||||
animation.toValue = targetOpacity
|
||||
return animation
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension UIView {
|
||||
var animationKeys: Set<String> {
|
||||
return Set(layer.animationKeys() ?? [])
|
||||
}
|
||||
|
||||
func removeAnimations(for animationKeys: Set<String>) {
|
||||
animationKeys.forEach { layer.removeAnimation(forKey: $0) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@_exported import UIKit
|
||||
|
||||
@MainActor
|
||||
final class BodyState {
|
||||
let bodyAppearanceCoordinator = BodyAppearanceCoordinator()
|
||||
var activeSubviews: [UIView] = []
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutCore
|
||||
@_exported import UIKit
|
||||
|
||||
@MainActor
|
||||
public protocol HasBody {
|
||||
@LayoutBuilder var body: Layout { get }
|
||||
}
|
||||
|
||||
extension HasBody where Self: UIView {
|
||||
/// Returns the body size if isBodyEnabled is true, otherwise nil.
|
||||
public func bodySizeThatFits(_ size: CGSize) -> CGSize? {
|
||||
return _QuickLayoutViewImplementation.sizeThatFits(self, size: size)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor @objc(QLBodyCoordinationExperiments)
|
||||
public class BodyCoordinationExperiments: NSObject {
|
||||
@objc static public var preventUnusedCollectionViewCellSizing: Bool = true
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
extension UIView {
|
||||
|
||||
@objc(quick_bodyContainerView) /// Provide unique name for ObjC runtime to avoid method name collision.
|
||||
open var bodyContainerView: UIView {
|
||||
return self
|
||||
}
|
||||
|
||||
@objc(quick_isBodyEnabled) /// Provide unique name for ObjC runtime to avoid method name collision.
|
||||
open var isBodyEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc(quick_isBodySizingEnabled) /// Provide unique name for ObjC runtime to avoid method name collision.
|
||||
internal var isBodySizingEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc(quick_isCachingEnabled) /// Provide unique name for ObjC runtime to avoid method name collision.
|
||||
open var isCachingEnabled: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UICollectionViewCell {
|
||||
override open var bodyContainerView: UIView {
|
||||
return contentView
|
||||
}
|
||||
override var isBodySizingEnabled: Bool {
|
||||
guard BodyCoordinationExperiments.preventUnusedCollectionViewCellSizing else { return true }
|
||||
// Disable self sizing if the collection view layout will request sizing info unnecessarily.
|
||||
// When the preferred attribute selector is overriden from the base class, we need to provide
|
||||
// sizing info as this means the layout may need the sizing. UITableView does not have the same
|
||||
// problem, this is specific to UICollectionView.
|
||||
let preferredFittingSelector = #selector(UICollectionViewLayout.shouldInvalidateLayout(forPreferredLayoutAttributes:withOriginalAttributes:))
|
||||
guard let collectionView = superview as? UICollectionView else { return true }
|
||||
return collectionView.collectionViewLayout.method(for: preferredFittingSelector) != UICollectionViewLayout.instanceMethod(for: preferredFittingSelector)
|
||||
}
|
||||
}
|
||||
|
||||
extension UITableViewCell {
|
||||
override open var bodyContainerView: UIView {
|
||||
return contentView
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutCore
|
||||
@_exported import UIKit
|
||||
|
||||
@MainActor
|
||||
struct ScopedLayoutCache {
|
||||
static var bodyCache: [ObjectIdentifier: Layout]?
|
||||
static var layoutCache: [ObjectIdentifier: LayoutNode]?
|
||||
static var viewsWithCache: Set<UIView>?
|
||||
|
||||
private static weak var currentOwner: UIView?
|
||||
|
||||
static func enter(_ view: UIView) {
|
||||
if Self.currentOwner == nil {
|
||||
Self.bodyCache = [:]
|
||||
Self.layoutCache = [:]
|
||||
Self.viewsWithCache = []
|
||||
Self.currentOwner = view
|
||||
}
|
||||
}
|
||||
|
||||
static func leave(_ view: UIView) {
|
||||
if Self.currentOwner === view {
|
||||
Self.bodyCache = nil
|
||||
Self.layoutCache = nil
|
||||
Self.viewsWithCache = nil
|
||||
Self.currentOwner = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
_QuickLayoutViewImplementation provides a canonical implementation of QuickLayout integration
|
||||
for a UIView with a Declarative interface. You shouldn't use this directly, but you may
|
||||
inherit usage via the Declarative protocol or upcoming macro support.
|
||||
*/
|
||||
@MainActor
|
||||
public struct _QuickLayoutViewImplementation {
|
||||
|
||||
// MARK: - Public
|
||||
public static func willMove(_ view: UIView, toWindow newWindow: UIWindow?) {
|
||||
guard view.isBodyEnabled else { return }
|
||||
let storage = getStorage(view)
|
||||
storage.bodyAppearanceCoordinator.coordinateMove(to: newWindow)
|
||||
}
|
||||
|
||||
public static func layoutSubviews(_ view: UIView & HasBody) {
|
||||
guard view.isBodyEnabled else {
|
||||
removeBodyIfNeeded(view)
|
||||
return
|
||||
}
|
||||
|
||||
let cachingIsEnabled = view.isCachingEnabled
|
||||
if cachingIsEnabled {
|
||||
ScopedLayoutCache.enter(view)
|
||||
}
|
||||
|
||||
let body = getBody(view)
|
||||
updateBodyIfNeeded(view, body)
|
||||
let storage = getStorage(view)
|
||||
storage.bodyAppearanceCoordinator.beginViewAppearanceUpdates()
|
||||
let layoutDirection: LayoutDirection = view.effectiveUserInterfaceLayoutDirection == .rightToLeft ? .rightToLeft : .leftToRight
|
||||
var cachedLayout: LayoutNode?
|
||||
let cacheKey = ScopedLayoutCache.layoutCache != nil ? ObjectIdentifier(view) : nil
|
||||
if let cacheKey, let layout = ScopedLayoutCache.layoutCache?[cacheKey], layout.size == view.bounds.size {
|
||||
cachedLayout = layout
|
||||
}
|
||||
body._applyFrame(view.bounds, alignment: .center, layoutDirection: layoutDirection, cachedLayout: cachedLayout)
|
||||
storage.bodyAppearanceCoordinator.commitViewAppearanceUpdates()
|
||||
|
||||
if cachingIsEnabled {
|
||||
if let viewsWithCache = ScopedLayoutCache.viewsWithCache {
|
||||
for subview in viewsWithCache {
|
||||
subview.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
ScopedLayoutCache.leave(view)
|
||||
}
|
||||
}
|
||||
|
||||
public static func sizeThatFits(_ view: UIView & HasBody, size: CGSize) -> CGSize? {
|
||||
guard view.isBodyEnabled, view.isBodySizingEnabled else { return nil }
|
||||
let body = getBody(view)
|
||||
updateBodyIfNeeded(view, body)
|
||||
let layoutDirection: LayoutDirection = view.effectiveUserInterfaceLayoutDirection == .rightToLeft ? .rightToLeft : .leftToRight
|
||||
let layout = body.layoutThatFits(size, layoutDirection: layoutDirection)
|
||||
let cacheKey = ScopedLayoutCache.layoutCache != nil ? ObjectIdentifier(view) : nil
|
||||
if let cacheKey {
|
||||
ScopedLayoutCache.viewsWithCache?.insert(view)
|
||||
ScopedLayoutCache.layoutCache?[cacheKey] = layout
|
||||
}
|
||||
return layout.size
|
||||
}
|
||||
|
||||
public static func quick_flexibility(_ view: UIView & HasBody, for axis: Axis) -> Flexibility? {
|
||||
guard view.isBodyEnabled else { return nil }
|
||||
return getBody(view).quick_flexibility(for: axis)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private static var bodyStateKey: UInt8 = 0
|
||||
|
||||
private static func getStorage(_ view: UIView) -> BodyState {
|
||||
guard let storage = objc_getAssociatedObject(view, &_QuickLayoutViewImplementation.bodyStateKey) as? BodyState else {
|
||||
let newStorage = BodyState()
|
||||
objc_setAssociatedObject(view, &_QuickLayoutViewImplementation.bodyStateKey, newStorage, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
return newStorage
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
private static func removeBodyIfNeeded(_ view: UIView) {
|
||||
updateBodyIfNeeded(view, EmptyLayout())
|
||||
}
|
||||
|
||||
private static func updateBodyIfNeeded(_ view: UIView, _ newBody: Layout) {
|
||||
let newViews = newBody.views()
|
||||
let storage = getStorage(view)
|
||||
guard newViews != storage.activeSubviews else { return }
|
||||
// We need a real diff to ensure correct view ordering
|
||||
for operation in newViews.difference(from: storage.activeSubviews) {
|
||||
switch operation {
|
||||
case .insert(let index, let insertedView, _):
|
||||
storage.bodyAppearanceCoordinator.coordinateAppearance(of: insertedView, in: view.bodyContainerView, index: index)
|
||||
case .remove(_, let view, _):
|
||||
storage.bodyAppearanceCoordinator.coordinateDisappearance(of: view)
|
||||
}
|
||||
}
|
||||
storage.activeSubviews = newViews
|
||||
}
|
||||
|
||||
private static func getBody(_ view: UIView & HasBody) -> Layout {
|
||||
let cacheKey = ScopedLayoutCache.bodyCache != nil ? ObjectIdentifier(view) : nil
|
||||
if let cacheKey, let body = ScopedLayoutCache.bodyCache?[cacheKey] {
|
||||
return body
|
||||
}
|
||||
let disableActions = CATransaction.disableActions()
|
||||
CATransaction.setDisableActions(true)
|
||||
let body = view.body
|
||||
if let cacheKey {
|
||||
ScopedLayoutCache.bodyCache?[cacheKey] = body
|
||||
}
|
||||
CATransaction.setDisableActions(disableActions)
|
||||
return body
|
||||
}
|
||||
}
|
||||
122
Sources/QuickLayout/QuickLayoutBridge/ViewProxy.swift
Normal file
122
Sources/QuickLayout/QuickLayoutBridge/ViewProxy.swift
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutCore
|
||||
@_exported import UIKit
|
||||
|
||||
// MARK: - Definition
|
||||
|
||||
/**
|
||||
ViewProxy should be utilized for sizing purposes. If you need to find the size of a layout,
|
||||
but you don't have the views yet, you can build a matching layout using ViewProxy objects
|
||||
in place of views.
|
||||
|
||||
For regular layout, where views exist, ViewProxy should not be used. It is a stand in for
|
||||
missing views only.
|
||||
*/
|
||||
public struct ViewProxy: LeafElement {
|
||||
private let horizontalFlexibility: Flexibility
|
||||
private let verticalFlexibility: Flexibility
|
||||
private let proxySizingCalculation: (CGSize) -> CGSize
|
||||
|
||||
public static func empty() -> ViewProxy {
|
||||
return ViewProxy(width: 0, height: 0)
|
||||
}
|
||||
|
||||
public init(width: CGFloat, height: CGFloat) {
|
||||
self.init(flexibility: .fixedSize) { _ in CGSize(width: width, height: height) }
|
||||
}
|
||||
|
||||
public init(width: CGFloat) {
|
||||
self.init(horizontal: .fixedSize, vertical: .fullyFlexible) { proposedSize in
|
||||
CGSize(width: width, height: proposedSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
public init(height: CGFloat) {
|
||||
self.init(horizontal: .fullyFlexible, vertical: .fixedSize) { proposedSize in
|
||||
CGSize(width: proposedSize.width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.init(horizontal: .fullyFlexible, vertical: .fullyFlexible) { proposedSize in
|
||||
proposedSize
|
||||
}
|
||||
}
|
||||
|
||||
public init(flexibility: Flexibility, proxySizingCalculation: @escaping (CGSize) -> CGSize) {
|
||||
self.horizontalFlexibility = flexibility
|
||||
self.verticalFlexibility = flexibility
|
||||
self.proxySizingCalculation = proxySizingCalculation
|
||||
}
|
||||
|
||||
public init(horizontal horizontalFlexibility: Flexibility, vertical verticalFlexibility: Flexibility, proxySizingCalculation: @escaping (CGSize) -> CGSize) {
|
||||
self.horizontalFlexibility = horizontalFlexibility
|
||||
self.verticalFlexibility = verticalFlexibility
|
||||
self.proxySizingCalculation = proxySizingCalculation
|
||||
}
|
||||
|
||||
// MARK: - Element Conformance
|
||||
|
||||
public func quick_layoutThatFits(_ proposedSize: CGSize) -> QuickLayoutCore.LayoutNode {
|
||||
return LayoutNode(view: nil, dimensions: ElementDimensions(proxySizingCalculation(proposedSize)))
|
||||
}
|
||||
|
||||
public func quick_flexibility(for axis: QuickLayoutCore.Axis) -> QuickLayoutCore.Flexibility {
|
||||
switch axis {
|
||||
case .horizontal: return horizontalFlexibility
|
||||
case .vertical: return verticalFlexibility
|
||||
}
|
||||
}
|
||||
|
||||
public func quick_layoutPriority() -> CGFloat {
|
||||
0
|
||||
}
|
||||
|
||||
public func quick_extractViewsIntoArray(_ views: inout [UIView]) {
|
||||
}
|
||||
|
||||
public func backingView() -> UIView? {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIKit Conformance
|
||||
|
||||
public extension UILabel {
|
||||
// While we could use NSString's boundingRectWithSize:, UILabel applies
|
||||
// caching, performance optimizations, and includes details like shadow offset.
|
||||
private static let sizingLabel = UILabel()
|
||||
static func proxy(for text: String, numberOfLines: Int = 1, with font: UIFont? = nil) -> ViewProxy {
|
||||
return ViewProxy(flexibility: .partial) { constrainingSize in
|
||||
assert(Thread.isMainThread, "UILabel ViewProxy can be used only on the main thread. Prefer using FOALabel.swift if you need to have background safe advance sizing.")
|
||||
sizingLabel.attributedText = nil
|
||||
sizingLabel.text = text
|
||||
sizingLabel.font = font
|
||||
sizingLabel.numberOfLines = numberOfLines
|
||||
return sizingLabel.quick_layoutThatFits(constrainingSize).size
|
||||
}
|
||||
}
|
||||
static func proxy(for attributedText: NSAttributedString, numberOfLines: Int = 1) -> ViewProxy {
|
||||
return ViewProxy(flexibility: .partial) { constrainingSize in
|
||||
assert(Thread.isMainThread, "UILabel ViewProxy can be used only on the main thread. Prefer using FOALabel.swift if you need to have background safe advance sizing.")
|
||||
sizingLabel.text = nil
|
||||
sizingLabel.attributedText = attributedText
|
||||
sizingLabel.numberOfLines = numberOfLines
|
||||
return sizingLabel.quick_layoutThatFits(constrainingSize).size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIImageView {
|
||||
static func proxy(for image: UIImage?) -> ViewProxy {
|
||||
return ViewProxy(flexibility: .fixedSize) { _ in
|
||||
return image?.size ?? .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Sources/QuickLayout/QuickLayoutBridge/Weak.swift
Normal file
10
Sources/QuickLayout/QuickLayoutBridge/Weak.swift
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
struct Weak<T: AnyObject> {
|
||||
weak var value: T?
|
||||
}
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
extension VerticalAlignment {
|
||||
private struct CustomVerticalAlignment: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
context[VerticalAlignment.bottom]
|
||||
}
|
||||
}
|
||||
static let custom = VerticalAlignment(
|
||||
CustomVerticalAlignment.self
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Tests
|
||||
|
||||
@MainActor
|
||||
class AlignmentGuidesServerSnaspshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testAlignmentGuideTaskList() {
|
||||
/// Expecting a task list with two categories, where
|
||||
/// tasks in each category are inset by 10 points from the leading edge.
|
||||
let groceriesTitleLabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Groceries"
|
||||
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
|
||||
return label
|
||||
}()
|
||||
|
||||
let groceryItemLabels = ["Milk", "Eggs", "Bananas"].map {
|
||||
let label = UILabel()
|
||||
label.text = $0
|
||||
return label
|
||||
}
|
||||
|
||||
let tasksTitleLabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Tasks"
|
||||
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
|
||||
return label
|
||||
}()
|
||||
|
||||
let taskItemLabels = ["Laundry", "Cook dinner"].map {
|
||||
let label = UILabel()
|
||||
label.text = $0
|
||||
return label
|
||||
}
|
||||
|
||||
let layout = VStack(alignment: .leading, spacing: 5) {
|
||||
groceriesTitleLabel
|
||||
for item in groceryItemLabels {
|
||||
item.alignmentGuide(.leading) { _ in -10 }
|
||||
}
|
||||
Spacer(20)
|
||||
tasksTitleLabel
|
||||
for item in taskItemLabels {
|
||||
item.alignmentGuide(.leading) { _ in -10 }
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
|
||||
func testInvalidAlignmentGuideBehavior() {
|
||||
/// Expecting a task list with no insets.
|
||||
|
||||
let groceryItemLabels = ["Milk", "Eggs", "Bananas"].map {
|
||||
let label = UILabel()
|
||||
label.text = $0
|
||||
return label
|
||||
}
|
||||
|
||||
let layout = VStack(alignment: .leading, spacing: 5) {
|
||||
for (index, item) in groceryItemLabels.enumerated() {
|
||||
item.alignmentGuide(.leading) { _ in
|
||||
switch index {
|
||||
case 0: .nan
|
||||
case 1: .infinity
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
|
||||
func testNestedVStackAlignmentBehavior() {
|
||||
let greenView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemGreen
|
||||
return view
|
||||
}()
|
||||
let blueView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemBlue
|
||||
return view
|
||||
}()
|
||||
let redView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemRed
|
||||
return view
|
||||
}()
|
||||
let layout = VStack(alignment: .leading) {
|
||||
greenView
|
||||
.frame(width: 50, height: 50)
|
||||
HStack {
|
||||
blueView
|
||||
.frame(width: 50, height: 50)
|
||||
.alignmentGuide(.leading) { _ in -10 }
|
||||
}
|
||||
redView
|
||||
.frame(width: 50, height: 50)
|
||||
.alignmentGuide(.leading) { _ in -10 }
|
||||
}
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
|
||||
func testNestedZStackAlignmentBehavior() {
|
||||
let greenView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemGreen
|
||||
return view
|
||||
}()
|
||||
let blueView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemBlue
|
||||
return view
|
||||
}()
|
||||
let redView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemRed
|
||||
return view
|
||||
}()
|
||||
let layout = ZStack(alignment: .topLeading) {
|
||||
greenView
|
||||
.frame(width: 70, height: 60)
|
||||
HStack {
|
||||
blueView
|
||||
.frame(width: 50, height: 50)
|
||||
.alignmentGuide(.leading) { _ in -10 }
|
||||
}
|
||||
redView
|
||||
.frame(width: 50, height: 50)
|
||||
.alignmentGuide(.leading) { _ in -10 }
|
||||
}
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
func testAlignmentGuidesArePropagatedByLayoutsWithSingleChildElements() {
|
||||
let greenView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemGreen
|
||||
return view
|
||||
}()
|
||||
let blueView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemBlue
|
||||
return view
|
||||
}()
|
||||
let redView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemRed
|
||||
return view
|
||||
}()
|
||||
let yellowView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemYellow
|
||||
return view
|
||||
}()
|
||||
let clearView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
let clearView2 = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
let layout = VStack(alignment: .leading) {
|
||||
ZStack {
|
||||
blueView
|
||||
.alignmentGuide(.leading) { _ in -15 }
|
||||
.frame(width: 50, height: 50)
|
||||
.padding(8)
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
|
||||
}
|
||||
VStack {
|
||||
redView
|
||||
.alignmentGuide(.leading) { _ in -5 }
|
||||
.frame(width: 50, height: 50)
|
||||
.padding(8)
|
||||
.offset(x: 0, y: 0)
|
||||
}
|
||||
HStack {
|
||||
greenView
|
||||
.alignmentGuide(.leading) { _ in 5 }
|
||||
.frame(width: 50, height: 50)
|
||||
.padding(8)
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
.overlay {
|
||||
clearView
|
||||
}
|
||||
}
|
||||
HFlow {
|
||||
yellowView
|
||||
.alignmentGuide(.leading) { _ in 15 }
|
||||
.frame(width: 50, height: 50)
|
||||
.padding(8)
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
.background {
|
||||
clearView2
|
||||
}
|
||||
}
|
||||
}
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 500)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
|
||||
func testDefaultAlignmentGuidesAreNotPropagatedThroughMultipleChildContainers() {
|
||||
let layout = buildAlignmentGuidePropagationLayout(with: .bottom)
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 500)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
|
||||
func testCustomAlignmentGuidesArePropagatedThroughMultipleChildContainers() {
|
||||
let layout = buildAlignmentGuidePropagationLayout(with: .custom)
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 500)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
|
||||
func testCustomAlignmentGuidePropagationPassesCorrectDimensions() {
|
||||
let iconView = UIImageView(image: UIImage(systemName: "face.smiling")!) // swiftlint:disable:this force_unwrapping
|
||||
let titleView = UILabel()
|
||||
let subtitleLabel = UILabel()
|
||||
|
||||
titleView.text = "Mauris fringilla ligula felis, nec pharetra velit congue id. Aenean hendrerit arcu lorem, in tempor est posuere id."
|
||||
titleView.font = UIFont.preferredFont(forTextStyle: .headline)
|
||||
titleView.numberOfLines = 0
|
||||
|
||||
subtitleLabel.text = "Lorem ipsum dolor sit amet consectetur adipiscing elit"
|
||||
subtitleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
|
||||
subtitleLabel.numberOfLines = 0
|
||||
|
||||
iconView.layer.borderColor = UIColor.systemGray.cgColor
|
||||
iconView.layer.borderWidth = 1
|
||||
|
||||
let layout = HStack(alignment: .custom) {
|
||||
iconView
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.alignmentGuide(.custom, computeValue: { d in d[.top] + d.height / 2 })
|
||||
Spacer(16)
|
||||
VStack(alignment: .leading) {
|
||||
titleView
|
||||
.alignmentGuide(.custom, computeValue: { d in d[.top] + d.height / 2 })
|
||||
subtitleLabel
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(16)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 500)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
|
||||
func testReferencingSameAlignmentIDWithinGuide() {
|
||||
let titleView = UILabel()
|
||||
titleView.text = "Title"
|
||||
titleView.font = .systemFont(ofSize: 17, weight: .bold)
|
||||
|
||||
let iconView = UIImageView(image: UIImage(systemName: "face.smiling")!) // swiftlint:disable:this force_unwrapping
|
||||
|
||||
let layout = VStack(alignment: .leading) {
|
||||
titleView
|
||||
iconView
|
||||
.alignmentGuide(.leading) { d in
|
||||
d[.leading] + 10
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 500)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
|
||||
func testCorrectDimensionsArePassedToAlignmentGuide() {
|
||||
let titleView = UILabel()
|
||||
titleView.text = "Right"
|
||||
titleView.font = .systemFont(ofSize: 17)
|
||||
|
||||
let titleView2 = UILabel()
|
||||
titleView2.text = "Left"
|
||||
titleView2.font = .systemFont(ofSize: 17)
|
||||
|
||||
let layout = VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading) {
|
||||
titleView
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
titleView2
|
||||
.alignmentGuide(.leading) { d in
|
||||
d.width
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 500)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildAlignmentGuidePropagationLayout(with alignment: VerticalAlignment) -> Layout {
|
||||
struct Model {
|
||||
let emojiString: String
|
||||
let emojiTitle: String
|
||||
let emojiDescription: String?
|
||||
}
|
||||
struct EmojiViews {
|
||||
let emojiView: UIView
|
||||
let emojiTitleView: UIView
|
||||
let emojiDescriptionView: UIView?
|
||||
}
|
||||
let titleView = UILabel()
|
||||
titleView.text = "Odd one out?"
|
||||
titleView.font = .systemFont(ofSize: 17, weight: .bold)
|
||||
let models: [Model] = [
|
||||
.init(emojiString: "🍔", emojiTitle: "Burger", emojiDescription: nil),
|
||||
.init(emojiString: "🍇", emojiTitle: "Grape", emojiDescription: nil),
|
||||
.init(emojiString: "🏡", emojiTitle: "House", emojiDescription: "This one!"),
|
||||
.init(emojiString: "🍎", emojiTitle: "Apple", emojiDescription: nil),
|
||||
]
|
||||
let emojiViews = models.map {
|
||||
let emojiView = UILabel()
|
||||
emojiView.text = $0.emojiString
|
||||
let titleView = UILabel()
|
||||
titleView.text = $0.emojiTitle
|
||||
let descriptionView: UILabel?
|
||||
if let description = $0.emojiDescription {
|
||||
descriptionView = UILabel()
|
||||
descriptionView?.text = description
|
||||
descriptionView?.textColor = .secondaryLabel
|
||||
descriptionView?.font = .systemFont(ofSize: 13)
|
||||
emojiView.font = .systemFont(ofSize: 34)
|
||||
} else {
|
||||
descriptionView = nil
|
||||
}
|
||||
return EmojiViews(emojiView: emojiView, emojiTitleView: titleView, emojiDescriptionView: descriptionView)
|
||||
}
|
||||
return VStack(spacing: 30) {
|
||||
titleView
|
||||
HStack(alignment: alignment, spacing: 16) {
|
||||
for emojiViewData in emojiViews {
|
||||
VStack {
|
||||
emojiViewData.emojiView
|
||||
emojiViewData.emojiTitleView
|
||||
.alignmentGuide(alignment) { _ in 0 }
|
||||
emojiViewData.emojiDescriptionView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class ApplyFramyServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTestWith(alignment: Alignment?) {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300)),
|
||||
alignment: alignment,
|
||||
containerBackground: ColorPallete.blue
|
||||
)
|
||||
}
|
||||
|
||||
func testDefaultAlignment() {
|
||||
runTestWith(alignment: nil)
|
||||
}
|
||||
|
||||
func testCenterAlignment() {
|
||||
runTestWith(alignment: .center)
|
||||
}
|
||||
|
||||
func testTopLeadingAlignment() {
|
||||
runTestWith(alignment: .topLeading)
|
||||
}
|
||||
|
||||
func testTopAlignment() {
|
||||
runTestWith(alignment: .top)
|
||||
}
|
||||
|
||||
func testTopTrailingAlignment() {
|
||||
runTestWith(alignment: .topTrailing)
|
||||
}
|
||||
|
||||
func testLeadingAlignment() {
|
||||
runTestWith(alignment: .leading)
|
||||
}
|
||||
|
||||
func testTrailingAlignment() {
|
||||
runTestWith(alignment: .trailing)
|
||||
}
|
||||
|
||||
func testBottomTrailingAlignment() {
|
||||
runTestWith(alignment: .bottomTrailing)
|
||||
}
|
||||
|
||||
func testBottomLeadingAlignment() {
|
||||
runTestWith(alignment: .bottomLeading)
|
||||
}
|
||||
|
||||
func testBottomAlignment() {
|
||||
runTestWith(alignment: .bottom)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import QuickLayoutBridge
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class AspectRatioServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testAspectRatioWithTextField() {
|
||||
let textField = UITextField()
|
||||
textField.backgroundColor = ColorPallete.red
|
||||
|
||||
let view = UIView()
|
||||
view.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack {
|
||||
textField
|
||||
Spacer(8)
|
||||
view
|
||||
.resizable()
|
||||
.aspectRatio(CGSize(width: 3, height: 2))
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 44))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class AspectRatioWithFiniteSizeServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTestWith(aspectRatio: CGSize, contentMode: ContentMode, proposedSize: CGSize) {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.layer.borderColor = UIColor.black.cgColor
|
||||
view2.layer.borderWidth = 1
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.aspectRatio(aspectRatio, contentMode: contentMode)
|
||||
view2
|
||||
}
|
||||
.frame(width: proposedSize.width, height: proposedSize.height)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
|
||||
// --- FILL
|
||||
|
||||
func testAspectRatio_fill_1_1_50_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fill, proposedSize: CGSize(width: 50, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_1_1_50_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fill, proposedSize: CGSize(width: 50, height: 100))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_1_1_100_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fill, proposedSize: CGSize(width: 100, height: 50))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fill_1_2_50_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fill, proposedSize: CGSize(width: 50, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_1_2_50_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fill, proposedSize: CGSize(width: 50, height: 100))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_1_2_100_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fill, proposedSize: CGSize(width: 100, height: 50))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fill_2_1_50_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fill, proposedSize: CGSize(width: 50, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_2_1_50_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fill, proposedSize: CGSize(width: 50, height: 100))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_2_1_100_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fill, proposedSize: CGSize(width: 100, height: 50))
|
||||
}
|
||||
|
||||
// --- FIT
|
||||
|
||||
func testAspectRatio_fit_1_1_50_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fit, proposedSize: CGSize(width: 50, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_1_1_50_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fit, proposedSize: CGSize(width: 50, height: 100))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_1_1_100_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fit, proposedSize: CGSize(width: 100, height: 50))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fit_1_2_50_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fit, proposedSize: CGSize(width: 50, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_1_2_50_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fit, proposedSize: CGSize(width: 50, height: 100))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_1_2_100_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fit, proposedSize: CGSize(width: 100, height: 50))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fit_2_1_50_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fit, proposedSize: CGSize(width: 50, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_2_1_50_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fit, proposedSize: CGSize(width: 50, height: 100))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_2_1_100_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fit, proposedSize: CGSize(width: 100, height: 50))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class AspectRatioWithInfiniteProposedHeightServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTestWith(aspectRatio: CGSize, contentMode: ContentMode, proposedSize: CGSize) {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.layer.borderColor = UIColor.black.cgColor
|
||||
view2.layer.borderWidth = 1
|
||||
view2.backgroundColor = .clear
|
||||
|
||||
let layout = ZStack(alignment: .top) {
|
||||
view1
|
||||
.aspectRatio(aspectRatio, contentMode: contentMode)
|
||||
view2
|
||||
.frame(height: 200)
|
||||
}
|
||||
|
||||
let targetView = UIView()
|
||||
targetView.addSubview(view1)
|
||||
targetView.addSubview(view2)
|
||||
targetView.frame.size = CGSize(width: proposedSize.width, height: 200)
|
||||
|
||||
layout.applyFrame(CGRect(origin: .zero, size: proposedSize))
|
||||
|
||||
let backgroundView = UIView()
|
||||
backgroundView.frame.size = CGSize(width: proposedSize.width, height: 200)
|
||||
backgroundView.backgroundColor = .clear
|
||||
backgroundView.addSubview(targetView)
|
||||
targetView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
|
||||
FBTakeSnapshotOfViewAfterScreenUpdates(backgroundView, nil)
|
||||
}
|
||||
|
||||
// --- FILL
|
||||
|
||||
func testAspectRatio_fill_1_1_50_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fill, proposedSize: CGSize(width: 50, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_1_1_100_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fill, proposedSize: CGSize(width: 100, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fill_1_2_50_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fill, proposedSize: CGSize(width: 50, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_1_2_100_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fill, proposedSize: CGSize(width: 100, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fill_2_1_50_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fill, proposedSize: CGSize(width: 50, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_2_1_100_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fill, proposedSize: CGSize(width: 100, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
// --- FIT
|
||||
|
||||
func testAspectRatio_fit_1_1_50_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fit, proposedSize: CGSize(width: 50, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_1_1_100_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fit, proposedSize: CGSize(width: 100, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fit_1_2_50_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fit, proposedSize: CGSize(width: 50, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_1_2_100_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fit, proposedSize: CGSize(width: 100, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fit_2_1_50_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fit, proposedSize: CGSize(width: 50, height: CGFloat.infinity))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_2_1_100_inf() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fit, proposedSize: CGSize(width: 100, height: CGFloat.infinity))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class AspectRatioWithInfiniteProposedWidthServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTestWith(aspectRatio: CGSize, contentMode: ContentMode, proposedSize: CGSize) {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.layer.borderColor = UIColor.black.cgColor
|
||||
view2.layer.borderWidth = 1
|
||||
view2.backgroundColor = .clear
|
||||
|
||||
let layout = ZStack(alignment: .leading) {
|
||||
view1
|
||||
.aspectRatio(aspectRatio, contentMode: contentMode)
|
||||
view2
|
||||
.frame(width: 200)
|
||||
}
|
||||
|
||||
let targetView = UIView()
|
||||
targetView.addSubview(view1)
|
||||
targetView.addSubview(view2)
|
||||
targetView.frame.size = CGSize(width: 200, height: 200)
|
||||
|
||||
layout.applyFrame(CGRect(origin: .zero, size: proposedSize))
|
||||
|
||||
let backgroundView = UIView()
|
||||
backgroundView.frame.size = CGSize(width: 200, height: proposedSize.height)
|
||||
backgroundView.backgroundColor = .clear
|
||||
backgroundView.addSubview(targetView)
|
||||
targetView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
|
||||
FBTakeSnapshotOfViewAfterScreenUpdates(backgroundView, nil)
|
||||
}
|
||||
|
||||
// --- FILL
|
||||
|
||||
func testAspectRatio_fill_1_1_50_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fill, proposedSize: CGSize(width: CGFloat.infinity, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_1_1_inf_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fill, proposedSize: CGSize(width: CGFloat.infinity, height: 100))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fill_1_2_inf_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fill, proposedSize: CGSize(width: CGFloat.infinity, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_1_2_inf_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fill, proposedSize: CGSize(width: CGFloat.infinity, height: 100))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fill_2_1_inf_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fill, proposedSize: CGSize(width: CGFloat.infinity, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fill_2_1_inf_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fill, proposedSize: CGSize(width: CGFloat.infinity, height: 100))
|
||||
}
|
||||
|
||||
// --- FIT
|
||||
|
||||
func testAspectRatio_fit_1_1_inf_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fit, proposedSize: CGSize(width: CGFloat.infinity, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_1_1_inf_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 1), contentMode: .fit, proposedSize: CGSize(width: CGFloat.infinity, height: 100))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fit_1_2_inf_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fit, proposedSize: CGSize(width: CGFloat.infinity, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_1_2_inf_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 1, height: 2), contentMode: .fit, proposedSize: CGSize(width: CGFloat.infinity, height: 100))
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
func testAspectRatio_fit_2_1_inf_50() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fit, proposedSize: CGSize(width: CGFloat.infinity, height: 50))
|
||||
}
|
||||
|
||||
func testAspectRatio_fit_2_1_inf_100() {
|
||||
runTestWith(aspectRatio: CGSize(width: 2, height: 1), contentMode: .fit, proposedSize: CGSize(width: CGFloat.infinity, height: 100))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
private struct FirstThirdAlignment: AlignmentID {
|
||||
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||
context.height / 3
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class CustomAlignmentServerSnaspshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testFirstThirdAlignment() {
|
||||
/// Expecting a 3x3 grid of blue rectangles.
|
||||
/// Rectangles having different heights, though they are aligned by the first third of their height.
|
||||
let colorViews = (1...9).map { _ in
|
||||
let view = UIView()
|
||||
view.backgroundColor = ColorPallete.blue
|
||||
return view
|
||||
}
|
||||
|
||||
let layout = HStack(alignment: VerticalAlignment(FirstThirdAlignment.self), spacing: 2) {
|
||||
VStack(spacing: 2) {
|
||||
colorViews[0]
|
||||
colorViews[1]
|
||||
colorViews[2]
|
||||
}.frame(height: 140)
|
||||
VStack(spacing: 2) {
|
||||
colorViews[3]
|
||||
colorViews[4]
|
||||
colorViews[5]
|
||||
}.frame(height: 250)
|
||||
VStack(spacing: 2) {
|
||||
colorViews[6]
|
||||
colorViews[7]
|
||||
colorViews[8]
|
||||
}.frame(height: 180)
|
||||
}
|
||||
.padding(20)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300)),
|
||||
alignment: .center,
|
||||
containerBackground: .white
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import QuickLayoutBridge
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class EmptyLayoutServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testEmptyLayoutWhenUsedInStack() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack {
|
||||
|
||||
view1
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
// EmptyLayouts shouldn't affect the positioning of the views
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
|
||||
view2
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 50))
|
||||
)
|
||||
}
|
||||
|
||||
func testEmptyLayoutWhenUsedAsLayout() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
takeSnapshot(
|
||||
with: EmptyLayout(),
|
||||
in: .exact(CGSize(width: 300, height: 50))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import QuickLayoutBridge
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class ExpandByServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testAspectRatioWithTextField() {
|
||||
|
||||
let size = 20
|
||||
|
||||
let firstView = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
firstView.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view4 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view4.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view5 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view5.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view6 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view6.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view7 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view7.backgroundColor = ColorPallete.blue
|
||||
|
||||
let lastView = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
lastView.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
firstView
|
||||
view2
|
||||
.expand(by: CGSize(width: size, height: size))
|
||||
view3
|
||||
.expand(by: CGSize(width: size, height: 0))
|
||||
view4
|
||||
.expand(by: CGSize(width: 0, height: size))
|
||||
view5
|
||||
.expand(by: CGSize(width: -100, height: 0))
|
||||
view6
|
||||
.expand(by: CGSize(width: CGFloat.nan, height: .nan)) // Invalid
|
||||
view7
|
||||
.expand(by: CGSize(width: CGFloat.infinity, height: .infinity)) // Invalid
|
||||
lastView
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,738 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class FlexibleFrameServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testFrameWithoutConstraints() {
|
||||
let view1 = ColorView(ColorPallete.yellow, text: "1")
|
||||
let view2 = ColorView(ColorPallete.yellow, text: "2")
|
||||
let view3 = ColorView(ColorPallete.yellow, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
|
||||
let borderView1 = BorderView()
|
||||
let borderView2 = BorderView()
|
||||
let borderView3 = BorderView()
|
||||
let borderView4 = BorderView()
|
||||
|
||||
let layout = VStack(spacing: 10) {
|
||||
view1
|
||||
.frame(width: 50)
|
||||
.frame(minWidth: nil, maxWidth: nil)
|
||||
.overlay { borderView1 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view2
|
||||
.frame(width: 100)
|
||||
.frame(minWidth: nil, maxWidth: nil)
|
||||
.overlay { borderView2 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view3
|
||||
.frame(width: 200)
|
||||
.frame(minWidth: nil, maxWidth: nil)
|
||||
.overlay { borderView3 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view4
|
||||
.frame(width: 250)
|
||||
.frame(minWidth: nil, maxWidth: nil)
|
||||
.overlay { borderView4 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
}
|
||||
.frame(width: 320)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameWithBothConstraints() {
|
||||
let view1 = ColorView(ColorPallete.yellow, text: "1")
|
||||
let view2 = ColorView(ColorPallete.yellow, text: "2")
|
||||
let view3 = ColorView(ColorPallete.yellow, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(ColorPallete.yellow, text: "5")
|
||||
let view6 = ColorView(ColorPallete.yellow, text: "6")
|
||||
let view7 = ColorView(ColorPallete.yellow, text: "7")
|
||||
let view8 = ColorView(ColorPallete.yellow, text: "8")
|
||||
let view9 = ColorView(ColorPallete.yellow, text: "9")
|
||||
|
||||
let borderView1 = BorderView()
|
||||
let borderView2 = BorderView()
|
||||
let borderView3 = BorderView()
|
||||
let borderView4 = BorderView()
|
||||
let borderView5 = BorderView()
|
||||
let borderView6 = BorderView()
|
||||
let borderView7 = BorderView()
|
||||
let borderView8 = BorderView()
|
||||
let borderView9 = BorderView()
|
||||
|
||||
let layout = VStack(alignment: .center, spacing: 10) {
|
||||
view1
|
||||
.frame(width: 40)
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView1 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view2
|
||||
.frame(width: 40)
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView2 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view3
|
||||
.frame(width: 40)
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView3 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view4
|
||||
.frame(width: 160)
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView4 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view5
|
||||
.frame(width: 160)
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView5 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view6
|
||||
.frame(width: 160)
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView6 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view7
|
||||
.frame(width: 260)
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView7 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view8
|
||||
.frame(width: 260)
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView8 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view9
|
||||
.frame(width: 260)
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView9 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
}
|
||||
.frame(width: 320)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameWithBothConstraintsAndFlexibleChild() {
|
||||
let view1 = ColorView(ColorPallete.yellow, text: "1")
|
||||
let view2 = ColorView(ColorPallete.yellow, text: "2")
|
||||
let view3 = ColorView(ColorPallete.yellow, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(ColorPallete.yellow, text: "5")
|
||||
let view6 = ColorView(ColorPallete.yellow, text: "6")
|
||||
let view7 = ColorView(ColorPallete.yellow, text: "7")
|
||||
let view8 = ColorView(ColorPallete.yellow, text: "8")
|
||||
let view9 = ColorView(ColorPallete.yellow, text: "9")
|
||||
|
||||
let borderView1 = BorderView()
|
||||
let borderView2 = BorderView()
|
||||
let borderView3 = BorderView()
|
||||
let borderView4 = BorderView()
|
||||
let borderView5 = BorderView()
|
||||
let borderView6 = BorderView()
|
||||
let borderView7 = BorderView()
|
||||
let borderView8 = BorderView()
|
||||
let borderView9 = BorderView()
|
||||
|
||||
let layout = VStack(alignment: .center, spacing: 10) {
|
||||
view1
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView1 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view2
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView2 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view3
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView3 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view4
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView4 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view5
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView5 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view6
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView6 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view7
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView7 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view8
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView8 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view9
|
||||
.frame(minWidth: 100, maxWidth: 200)
|
||||
.overlay { borderView9 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
}
|
||||
.frame(width: 320)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameWithMinOnly() {
|
||||
let view1 = ColorView(ColorPallete.yellow, text: "1")
|
||||
let view2 = ColorView(ColorPallete.yellow, text: "2")
|
||||
let view3 = ColorView(ColorPallete.yellow, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(ColorPallete.yellow, text: "5")
|
||||
let view6 = ColorView(ColorPallete.yellow, text: "6")
|
||||
let view7 = ColorView(ColorPallete.yellow, text: "7")
|
||||
let view8 = ColorView(ColorPallete.yellow, text: "8")
|
||||
let view9 = ColorView(ColorPallete.yellow, text: "9")
|
||||
|
||||
let borderView1 = BorderView()
|
||||
let borderView2 = BorderView()
|
||||
let borderView3 = BorderView()
|
||||
let borderView4 = BorderView()
|
||||
let borderView5 = BorderView()
|
||||
let borderView6 = BorderView()
|
||||
let borderView7 = BorderView()
|
||||
let borderView8 = BorderView()
|
||||
let borderView9 = BorderView()
|
||||
|
||||
let layout = VStack(alignment: .center, spacing: 10) {
|
||||
view1
|
||||
.frame(width: 40)
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView1 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view2
|
||||
.frame(width: 40)
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView2 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view3
|
||||
.frame(width: 40)
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView3 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view4
|
||||
.frame(width: 160)
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView4 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view5
|
||||
.frame(width: 160)
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView5 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view6
|
||||
.frame(width: 160)
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView6 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view7
|
||||
.frame(width: 260)
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView7 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view8
|
||||
.frame(width: 260)
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView8 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view9
|
||||
.frame(width: 260)
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView9 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
}
|
||||
.frame(width: 320)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameWithMinOnlyFullyFlexibleChild() {
|
||||
let view1 = ColorView(ColorPallete.yellow, text: "1")
|
||||
let view2 = ColorView(ColorPallete.yellow, text: "2")
|
||||
let view3 = ColorView(ColorPallete.yellow, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(ColorPallete.yellow, text: "5")
|
||||
let view6 = ColorView(ColorPallete.yellow, text: "6")
|
||||
let view7 = ColorView(ColorPallete.yellow, text: "7")
|
||||
let view8 = ColorView(ColorPallete.yellow, text: "8")
|
||||
let view9 = ColorView(ColorPallete.yellow, text: "9")
|
||||
|
||||
let borderView1 = BorderView()
|
||||
let borderView2 = BorderView()
|
||||
let borderView3 = BorderView()
|
||||
let borderView4 = BorderView()
|
||||
let borderView5 = BorderView()
|
||||
let borderView6 = BorderView()
|
||||
let borderView7 = BorderView()
|
||||
let borderView8 = BorderView()
|
||||
let borderView9 = BorderView()
|
||||
|
||||
let layout = VStack(alignment: .center, spacing: 10) {
|
||||
view1
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView1 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view2
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView2 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view3
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView3 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view4
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView4 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view5
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView5 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view6
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView6 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view7
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView7 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view8
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView8 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view9
|
||||
.frame(minWidth: 100)
|
||||
.overlay { borderView9 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
}
|
||||
.frame(width: 320)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameWithMaxOnly() {
|
||||
let view1 = ColorView(ColorPallete.yellow, text: "1")
|
||||
let view2 = ColorView(ColorPallete.yellow, text: "2")
|
||||
let view3 = ColorView(ColorPallete.yellow, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(ColorPallete.yellow, text: "5")
|
||||
let view6 = ColorView(ColorPallete.yellow, text: "6")
|
||||
let view7 = ColorView(ColorPallete.yellow, text: "7")
|
||||
let view8 = ColorView(ColorPallete.yellow, text: "8")
|
||||
let view9 = ColorView(ColorPallete.yellow, text: "9")
|
||||
|
||||
let borderView1 = BorderView()
|
||||
let borderView2 = BorderView()
|
||||
let borderView3 = BorderView()
|
||||
let borderView4 = BorderView()
|
||||
let borderView5 = BorderView()
|
||||
let borderView6 = BorderView()
|
||||
let borderView7 = BorderView()
|
||||
let borderView8 = BorderView()
|
||||
let borderView9 = BorderView()
|
||||
|
||||
let layout = VStack(alignment: .center, spacing: 10) {
|
||||
view1
|
||||
.frame(width: 40)
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView1 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view2
|
||||
.frame(width: 40)
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView2 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view3
|
||||
.frame(width: 40)
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView3 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view4
|
||||
.frame(width: 160)
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView4 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view5
|
||||
.frame(width: 160)
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView5 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view6
|
||||
.frame(width: 160)
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView6 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view7
|
||||
.frame(width: 260)
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView7 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view8
|
||||
.frame(width: 260)
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView8 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view9
|
||||
.frame(width: 260)
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView9 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
}
|
||||
.frame(width: 320)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameWithMaxOnlyFullyFlexibleChild() {
|
||||
let view1 = ColorView(ColorPallete.yellow, text: "1")
|
||||
let view2 = ColorView(ColorPallete.yellow, text: "2")
|
||||
let view3 = ColorView(ColorPallete.yellow, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(ColorPallete.yellow, text: "5")
|
||||
let view6 = ColorView(ColorPallete.yellow, text: "6")
|
||||
let view7 = ColorView(ColorPallete.yellow, text: "7")
|
||||
let view8 = ColorView(ColorPallete.yellow, text: "8")
|
||||
let view9 = ColorView(ColorPallete.yellow, text: "9")
|
||||
|
||||
let borderView1 = BorderView()
|
||||
let borderView2 = BorderView()
|
||||
let borderView3 = BorderView()
|
||||
let borderView4 = BorderView()
|
||||
let borderView5 = BorderView()
|
||||
let borderView6 = BorderView()
|
||||
let borderView7 = BorderView()
|
||||
let borderView8 = BorderView()
|
||||
let borderView9 = BorderView()
|
||||
|
||||
let layout = VStack(alignment: .center, spacing: 10) {
|
||||
view1
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView1 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view2
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView2 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view3
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView3 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view4
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView4 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view5
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView5 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view6
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView6 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
|
||||
view7
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView7 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
|
||||
view8
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView8 }
|
||||
.frame(width: 150)
|
||||
.frame(height: 20)
|
||||
|
||||
view9
|
||||
.frame(maxWidth: 200)
|
||||
.overlay { borderView9 }
|
||||
.frame(width: 250)
|
||||
.frame(height: 20)
|
||||
}
|
||||
.frame(width: 320)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameAlignment() {
|
||||
let view1 = ColorView(ColorPallete.yellow, text: "1")
|
||||
let view2 = ColorView(ColorPallete.yellow, text: "2")
|
||||
let view3 = ColorView(ColorPallete.yellow, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(ColorPallete.yellow, text: "5")
|
||||
let view6 = ColorView(ColorPallete.yellow, text: "6")
|
||||
let view7 = ColorView(ColorPallete.yellow, text: "7")
|
||||
let view8 = ColorView(ColorPallete.yellow, text: "8")
|
||||
let view9 = ColorView(ColorPallete.yellow, text: "9")
|
||||
let view10 = ColorView(ColorPallete.yellow, text: "10")
|
||||
let view11 = ColorView(ColorPallete.yellow, text: "11")
|
||||
let view12 = ColorView(ColorPallete.yellow, text: "12")
|
||||
|
||||
let borderView1 = BorderView()
|
||||
let borderView2 = BorderView()
|
||||
let borderView3 = BorderView()
|
||||
let borderView4 = BorderView()
|
||||
let borderView5 = BorderView()
|
||||
let borderView6 = BorderView()
|
||||
let borderView7 = BorderView()
|
||||
let borderView8 = BorderView()
|
||||
let borderView9 = BorderView()
|
||||
let borderView10 = BorderView()
|
||||
let borderView11 = BorderView()
|
||||
let borderView12 = BorderView()
|
||||
|
||||
let borderView1_1 = BorderView()
|
||||
let borderView1_2 = BorderView()
|
||||
let borderView1_3 = BorderView()
|
||||
let borderView1_4 = BorderView()
|
||||
let borderView1_5 = BorderView()
|
||||
let borderView1_6 = BorderView()
|
||||
let borderView1_7 = BorderView()
|
||||
let borderView1_8 = BorderView()
|
||||
let borderView1_9 = BorderView()
|
||||
let borderView1_10 = BorderView()
|
||||
let borderView1_11 = BorderView()
|
||||
let borderView1_12 = BorderView()
|
||||
|
||||
let layout = VStack(spacing: 10) {
|
||||
view1
|
||||
.frame(width: 150)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
|
||||
.overlay { borderView1 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_1 }
|
||||
|
||||
view2
|
||||
.frame(width: 150)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
.overlay { borderView2 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_2 }
|
||||
|
||||
view3
|
||||
.frame(width: 150)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .trailing)
|
||||
.overlay { borderView3 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_3 }
|
||||
|
||||
view4
|
||||
.frame(width: 150)
|
||||
.frame(minWidth: 0, alignment: .center)
|
||||
.overlay { borderView4 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_4 }
|
||||
|
||||
view5
|
||||
.frame(width: 150)
|
||||
.frame(minWidth: 0, alignment: .leading)
|
||||
.overlay { borderView5 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_5 }
|
||||
|
||||
view6
|
||||
.frame(width: 150)
|
||||
.frame(minWidth: 0, alignment: .trailing)
|
||||
.overlay { borderView6 }
|
||||
.frame(width: 200)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_6 }
|
||||
|
||||
view7
|
||||
.frame(width: 150)
|
||||
.frame(minWidth: 0, alignment: .center)
|
||||
.overlay { borderView7 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_7 }
|
||||
|
||||
view8
|
||||
.frame(width: 150)
|
||||
.frame(minWidth: 0, alignment: .leading)
|
||||
.overlay { borderView8 }
|
||||
.frame(width: 200)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_8 }
|
||||
|
||||
view9
|
||||
.frame(width: 150)
|
||||
.frame(minWidth: 0, alignment: .trailing)
|
||||
.overlay { borderView9 }
|
||||
.frame(width: 200)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_9 }
|
||||
|
||||
view10
|
||||
.frame(width: 150)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.overlay { borderView10 }
|
||||
.frame(width: 50)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_10 }
|
||||
|
||||
view11
|
||||
.frame(width: 150)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.overlay { borderView11 }
|
||||
.frame(width: 200)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_11 }
|
||||
|
||||
view12
|
||||
.frame(width: 150)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.overlay { borderView12 }
|
||||
.frame(width: 200)
|
||||
.frame(height: 20)
|
||||
.overlay { borderView1_12 }
|
||||
}
|
||||
.frame(width: 320)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class ForEachServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testForEach() {
|
||||
|
||||
let size = 20
|
||||
|
||||
let view1 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 20) {
|
||||
ForEach([view1, view2, view3])
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testForEachWithViewBlock() {
|
||||
|
||||
let size = 20
|
||||
|
||||
let view1 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 20) {
|
||||
view1
|
||||
ForEach([view2, view3]) { view in
|
||||
view.resizable().frame(width: 30, height: 30)
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testForEachWithViewElementBlock() {
|
||||
|
||||
let size = 20
|
||||
|
||||
let view1 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 20) {
|
||||
view1
|
||||
ForEach([view2.padding(.top, 8), view3.padding(.top, 8)]) { element in
|
||||
element.offset(y: -16)
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testForLoop() {
|
||||
|
||||
let size = 20
|
||||
|
||||
let view1 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 20) {
|
||||
for view in [view1, view2, view3] {
|
||||
view
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testForLoopWithModifiers() {
|
||||
|
||||
let size = 20
|
||||
|
||||
let view1 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 20) {
|
||||
view1
|
||||
for view in [view2, view3] {
|
||||
view
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class FrameServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testFrameAlignment() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
let view2 = UIImageView(image: generateTestImage(with: "2", size: size))
|
||||
let view3 = UIImageView(image: generateTestImage(with: "3", size: size))
|
||||
let view4 = UIImageView(image: generateTestImage(with: "4", size: size))
|
||||
let view5 = UIImageView(image: generateTestImage(with: "5", size: size))
|
||||
let view6 = UIImageView(image: generateTestImage(with: "6", size: size))
|
||||
let view7 = UIImageView(image: generateTestImage(with: "7", size: size))
|
||||
let view8 = UIImageView(image: generateTestImage(with: "8", size: size))
|
||||
let view9 = UIImageView(image: generateTestImage(with: "9", size: size))
|
||||
|
||||
let frameSize = CGSize(width: size.width * 3, height: size.height * 3)
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.frame(width: frameSize.width, height: frameSize.height, alignment: .topLeading)
|
||||
view2
|
||||
.frame(width: frameSize.width, height: frameSize.height, alignment: .top)
|
||||
view3
|
||||
.frame(width: frameSize.width, height: frameSize.height, alignment: .topTrailing)
|
||||
view4
|
||||
.frame(width: frameSize.width, height: frameSize.height, alignment: .leading)
|
||||
view5
|
||||
.frame(width: frameSize.width, height: frameSize.height, alignment: .center)
|
||||
view6
|
||||
.frame(width: frameSize.width, height: frameSize.height, alignment: .trailing)
|
||||
view7
|
||||
.frame(width: frameSize.width, height: frameSize.height, alignment: .bottomLeading)
|
||||
view8
|
||||
.frame(width: frameSize.width, height: frameSize.height, alignment: .bottom)
|
||||
view9
|
||||
.frame(width: frameSize.width, height: frameSize.height, alignment: .bottomTrailing)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(frameSize)
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameTruncatesLabel() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "This is a very long label"
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "This is a very long label"
|
||||
|
||||
let layout = VStack(alignment: .leading) {
|
||||
view1
|
||||
view2
|
||||
.frame(width: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameLimitsUIViewSize() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack(alignment: .leading) {
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameLimitsUIViewSizeHorizontally() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack(alignment: .leading) {
|
||||
view1
|
||||
.frame(width: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrameLimitsUIViewSizeVertically() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack(alignment: .leading) {
|
||||
view1
|
||||
.frame(height: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,709 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
final class HFlowServerSnapShotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func buildHFlowSingleChild(
|
||||
itemAlignment: VerticalAlignment,
|
||||
lineAlignment: HorizontalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat
|
||||
) {
|
||||
|
||||
let view = ColorView(ColorPallete.red, text: "1")
|
||||
|
||||
let HFlow = HFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: HFlow,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func buildHFlowSingleFixedChild(
|
||||
itemAlignment: VerticalAlignment,
|
||||
lineAlignment: HorizontalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat
|
||||
) {
|
||||
|
||||
let view = ColorView(ColorPallete.red, text: "1")
|
||||
|
||||
let HFlow = HFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view.frame(width: 100, height: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: HFlow,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: VerticalAlignment, lineAlignment: HorizontalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat,
|
||||
layoutDirection: LayoutDirection = .leftToRight
|
||||
) {
|
||||
|
||||
let view1 = ColorView(ColorPallete.red, text: "1")
|
||||
let view2 = ColorView(ColorPallete.orange, text: "2")
|
||||
let view3 = ColorView(ColorPallete.blue, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(.green, text: "5")
|
||||
|
||||
let HFlow = HFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
view3
|
||||
.frame(width: 50, height: 50)
|
||||
view4
|
||||
.frame(width: 50, height: 50)
|
||||
view5
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
.layoutDirection(layoutDirection)
|
||||
|
||||
takeSnapshot(
|
||||
with: HFlow,
|
||||
in: .proposed(CGSize(width: 150, height: 150))
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: VerticalAlignment,
|
||||
lineAlignment: HorizontalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat,
|
||||
proposedSize: CGSize = CGSize(width: 200, height: 200)
|
||||
) {
|
||||
|
||||
let view1 = ColorView(ColorPallete.red, text: "1")
|
||||
let view2 = ColorView(ColorPallete.orange, text: "2")
|
||||
let view3 = ColorView(ColorPallete.blue, text: "3")
|
||||
|
||||
let HFlow = HFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
view3
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: HFlow,
|
||||
in: .proposed(proposedSize)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: VerticalAlignment,
|
||||
lineAlignment: HorizontalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat
|
||||
) {
|
||||
|
||||
let view1 = ColorView(ColorPallete.red, text: "1")
|
||||
let view2 = ColorView(ColorPallete.orange, text: "2")
|
||||
let view3 = ColorView(ColorPallete.blue, text: "3")
|
||||
|
||||
let HFlow = HFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view1
|
||||
.frame(width: 80, height: 50)
|
||||
view2
|
||||
.frame(width: 30, height: 70)
|
||||
view3
|
||||
.frame(width: 50, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: HFlow,
|
||||
in: .proposed(CGSize(width: 200, height: 200))
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: VerticalAlignment,
|
||||
lineAlignment: HorizontalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat
|
||||
) {
|
||||
|
||||
let view1 = ColorView(ColorPallete.red, text: "1")
|
||||
let view2 = ColorView(ColorPallete.orange, text: "2")
|
||||
let view3 = ColorView(ColorPallete.blue, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(.green, text: "5")
|
||||
|
||||
let HFlow = HFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view1
|
||||
.frame(width: 80, height: 50)
|
||||
view2
|
||||
.frame(width: 30, height: 70)
|
||||
view3
|
||||
.frame(width: 50, height: 40)
|
||||
view4
|
||||
.frame(width: 50, height: 50)
|
||||
view5
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: HFlow,
|
||||
in: .proposed(CGSize(width: 200, height: 200))
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Single Child
|
||||
func testSingleChild() {
|
||||
buildHFlowSingleChild(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
// MARK: - Multiple Children Same Size Single Line
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLine() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLineItemAlignmentTop() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLineItemAlignmentBottom() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLineItemSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLineLineSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 10)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLineItemAndLineSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 15
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Multiple Children with different proposed size
|
||||
func testMultipleChildrenSameSizeSingleLine150x150() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0,
|
||||
proposedSize: CGSize(width: 150, height: 150)
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLine149x150() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0,
|
||||
proposedSize: CGSize(width: 149, height: 150)
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLine99x150() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0,
|
||||
proposedSize: CGSize(width: 99, height: 150)
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLine49x150() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0,
|
||||
proposedSize: CGSize(width: 49, height: 150)
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLine170x150WithItemSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0,
|
||||
proposedSize: CGSize(width: 170, height: 150)
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLine169x150WithItemSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0,
|
||||
proposedSize: CGSize(width: 169, height: 150)
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLine110x150WithItemSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0,
|
||||
proposedSize: CGSize(width: 110, height: 150)
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLine109x150WithItemSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0,
|
||||
proposedSize: CGSize(width: 109, height: 150)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Multiple Children Same Size MultiLine
|
||||
|
||||
func testMultipleChildren() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenItemAlignmentTop() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .center, itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenItemAlignmentBottom() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .center, itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenLineAlignmentLeading() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .leading, itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenLineAlignmentTrailing() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenItemAlignmentTopLineAlignmentLeading() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .leading,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenItemAlignmentTopLineAlingmentTrailing() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenItemAlignmentBottomLineAlignmentLeading() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .leading,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenItemAlignmentBottomLineAlignmentTrailing() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenWithItemSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenWithLineSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 10
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenWithItemAndLineSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 15
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenWithNegativeItemSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: -10,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenWithNegativeLineSpacing() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: -10
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Multiple Children Different Size Single Line
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLine() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemAlignmentTop() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemAlignmentBottom() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineLineAlignmentLeading() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .leading,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineLineAlignmentTrailing() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemAlignmentTopLineAlignmentLeading() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .leading,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemAlignmentTopLineAlignmentTrailing() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemAlignmentBottomLineAlignmentLeading() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .leading,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemAlignmentBottomLineAlignmentTrailing() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeWithItemSpacing() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeWithLineSpacing() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 10
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeWithItemAndLineSpacing() {
|
||||
buildHflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 15
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Multiple Children Different Size MultiLine
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLine() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemAlignmentTop() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemAlignmentBottom() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineLineAlignmentLeading() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .leading,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineLineAlignmentTrailing() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemAlignmentTopLineAlignmentLeading() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .leading,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemAlignmentTopLineAlignmentTrailing() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .top,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemAlignmentBottomLineAlignmentLeading() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .leading,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemAlignmentBottomLineAlignmentTrailing() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .bottom,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineWithItemSpacing() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineWithLineSpacing() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 10
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineWithItemAndLineSpacing() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 15
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineWithNegativeItemSpacing() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: -10,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineWithNegativeLineSpacing() {
|
||||
buildHflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: -10
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - RTL
|
||||
|
||||
func testMultipleChildrenRTL() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0,
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenLineAlignmentLeadingRTL() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .leading,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0,
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenLineAlignmentTrailingRTL() {
|
||||
buildHflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .trailing,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0,
|
||||
layoutDirection: .rightToLeft)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
|
||||
@testable import QuickLayoutBridge
|
||||
@testable import QuickLayoutCore
|
||||
|
||||
@MainActor
|
||||
class HStackRTLServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testWithThreeViews() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
view2
|
||||
.frame(width: 80, height: 80)
|
||||
view3
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300)),
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
|
||||
func testWithThreeViewsAndSpacing() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
view2
|
||||
.frame(width: 80, height: 80)
|
||||
view3
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300)),
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
|
||||
func testWithThreeViewsAndSpacers() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
Spacer(40)
|
||||
view2
|
||||
.frame(width: 80, height: 80)
|
||||
Spacer(10)
|
||||
view3
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300)),
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
|
||||
func testWithOneViewAndSpacerAfter() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
Spacer(40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300)),
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
|
||||
func testWithOneViewAndSpacerBefore() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
Spacer(40)
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300)),
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
|
||||
func testWithSingleSpacer() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
Spacer(40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 100, height: 100)),
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class HStackServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTest(alignment: VerticalAlignment) {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = HStack(alignment: alignment) {
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
view2
|
||||
.frame(width: 80, height: 80)
|
||||
view3
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testAlignmentCenter() {
|
||||
runTest(alignment: .center)
|
||||
}
|
||||
|
||||
func testAlignmentTop() {
|
||||
runTest(alignment: .top)
|
||||
}
|
||||
|
||||
func testAlignmentBottom() {
|
||||
runTest(alignment: .bottom)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class HStackWithTwoViewsAndInfinitSizeServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTest(on views: (UIView, UIView)) {
|
||||
takeSnapshot(
|
||||
with: HStack {
|
||||
views.0
|
||||
views.1
|
||||
},
|
||||
in: .proposed(CGSize(width: CGFloat.infinity, height: .infinity))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLabelsInHStack() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Label 1"
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "Label 2"
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIViews() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUISliders() {
|
||||
let view1 = UISlider()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UISlider()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
// Overlapping shadows from the two sliders causes the snapshot to fail. So we need to add a Spacer between them.
|
||||
takeSnapshot(
|
||||
with: HStack {
|
||||
view1
|
||||
Spacer(40)
|
||||
view2
|
||||
},
|
||||
in: .proposed(CGSize(width: CGFloat.infinity, height: .infinity))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoUIScrollView() {
|
||||
let view1 = UIScrollView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIScrollView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUITextView() {
|
||||
let view1 = UITextView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UITextView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUICollectionView() {
|
||||
let view1 = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUITextField() {
|
||||
let view1 = UITextField()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UITextField()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUISearchField() {
|
||||
let view1 = UITextField()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UITextField()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIButton() {
|
||||
let view1 = UIButton(type: .system)
|
||||
view1.setTitle("Button 1", for: .normal)
|
||||
|
||||
let view2 = UIButton(type: .system)
|
||||
view2.setTitle("Button 2", for: .normal)
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUISwitch() {
|
||||
let view1 = UISwitch()
|
||||
let view2 = UISwitch()
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIStepper() {
|
||||
let view1 = UIStepper()
|
||||
let view2 = UIStepper()
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIActivitiyIndicators() {
|
||||
if #available(iOS 13.0, *) {
|
||||
let view1 = UIActivityIndicatorView(style: .medium)
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
view1.startAnimating()
|
||||
|
||||
let view2 = UIActivityIndicatorView(style: .medium)
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
view2.startAnimating()
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
}
|
||||
|
||||
func testTwoUIProgressView() {
|
||||
let view1 = UIProgressView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIProgressView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIImageView() {
|
||||
let view1 = UIImageView(image: FBTestImageGenerator.image(with: ColorPallete.blue, size: CGSize(width: 40, height: 40)))
|
||||
let view2 = UIImageView(image: FBTestImageGenerator.image(with: ColorPallete.yellow, size: CGSize(width: 40, height: 40)))
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class HStackWithTwoViewsServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTest(on views: (UIView, UIView)) {
|
||||
takeSnapshot(
|
||||
with: HStack {
|
||||
views.0
|
||||
views.1
|
||||
},
|
||||
in: .proposed(CGSize(width: 300, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLabelsInHStack() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Label 1"
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "Label 2"
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIViews() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUISliders() {
|
||||
let view1 = UISlider()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UISlider()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIScrollView() {
|
||||
let view1 = UIScrollView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIScrollView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUITextView() {
|
||||
let view1 = UITextView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UITextView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUICollectionView() {
|
||||
let view1 = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUITablieViews() {
|
||||
let view1 = UITableView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UITableView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUITextField() {
|
||||
let view1 = UITextField()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UITextField()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUISearchField() {
|
||||
let view1 = UITextField()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UITextField()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIButton() {
|
||||
let view1 = UIButton(type: .system)
|
||||
view1.setTitle("Button 1", for: .normal)
|
||||
|
||||
let view2 = UIButton(type: .system)
|
||||
view2.setTitle("Button 2", for: .normal)
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUISwitch() {
|
||||
let view1 = UISwitch()
|
||||
let view2 = UISwitch()
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIStepper() {
|
||||
let view1 = UIStepper()
|
||||
let view2 = UIStepper()
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIActivitiyIndicators() {
|
||||
if #available(iOS 13.0, *) {
|
||||
let view1 = UIActivityIndicatorView(style: .medium)
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
view1.startAnimating()
|
||||
|
||||
let view2 = UIActivityIndicatorView(style: .medium)
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
view2.startAnimating()
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
}
|
||||
|
||||
func testTwoUIProgressView() {
|
||||
let view1 = UIProgressView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIProgressView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIImageView() {
|
||||
let view1 = UIImageView(image: FBTestImageGenerator.image(with: ColorPallete.blue, size: CGSize(width: 40, height: 40)))
|
||||
let view2 = UIImageView(image: FBTestImageGenerator.image(with: ColorPallete.yellow, size: CGSize(width: 40, height: 40)))
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import QuickLayoutBridge
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class IdealLayoutServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testEqualWidthLabels() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Lorem ipsum dolor"
|
||||
view1.textColor = .white
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "ipsum"
|
||||
view2.textColor = .white
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack {
|
||||
view1
|
||||
.resizable(axis: .horizontal)
|
||||
Spacer(8)
|
||||
view2
|
||||
.resizable(axis: .horizontal)
|
||||
}
|
||||
.idealLayout()
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 50))
|
||||
)
|
||||
}
|
||||
|
||||
func testEqualWidthSingleLabel() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Lorem"
|
||||
view1.textColor = .white
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = VStack {
|
||||
view1
|
||||
.resizable(axis: .horizontal)
|
||||
}
|
||||
.idealLayout()
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 50))
|
||||
)
|
||||
}
|
||||
|
||||
func testEqualWidthTwoLabelGetsTruncated() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Lorem ipsum dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor"
|
||||
view1.textColor = .white
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "ipsum"
|
||||
view2.textColor = .white
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack {
|
||||
view1
|
||||
.resizable(axis: .horizontal)
|
||||
Spacer(8)
|
||||
view2
|
||||
.resizable(axis: .horizontal)
|
||||
}
|
||||
.idealLayout()
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 100, height: 50))
|
||||
)
|
||||
}
|
||||
|
||||
func testEqualWidthSingleLabelGetsTruncated() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Lorem ipsum dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor"
|
||||
view1.textColor = .white
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = VStack {
|
||||
view1
|
||||
.resizable(axis: .horizontal)
|
||||
}
|
||||
.idealLayout()
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 100, height: 50))
|
||||
)
|
||||
}
|
||||
|
||||
func testEqualHeightLabels() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Lorem\nipsum\ndolor"
|
||||
view1.textColor = .white
|
||||
view1.numberOfLines = 0
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "ipsum"
|
||||
view2.textColor = .white
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
.resizable(axis: .vertical)
|
||||
Spacer(8)
|
||||
view2
|
||||
.resizable(axis: .vertical)
|
||||
}
|
||||
.idealLayout()
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testEqualHeightLayouts() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Lorem\nipsum\ndolor\ndolor\ndolor"
|
||||
view1.textColor = .white
|
||||
view1.numberOfLines = 0
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "ipsum"
|
||||
view2.textColor = .white
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UILabel()
|
||||
view3.text = "ipsum"
|
||||
view3.textColor = .white
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
.resizable(axis: .vertical)
|
||||
Spacer(8)
|
||||
|
||||
VStack {
|
||||
view2
|
||||
Spacer()
|
||||
view3
|
||||
}
|
||||
}
|
||||
.idealLayout()
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 500, height: 500))
|
||||
)
|
||||
}
|
||||
|
||||
func testEqualWidthClampedToProposedSize() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Lorem ipsum dolor dolor dolor dolor"
|
||||
view1.textColor = .white
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "ipsum"
|
||||
view2.textColor = .white
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack {
|
||||
view1
|
||||
.resizable(axis: .horizontal)
|
||||
Spacer(8)
|
||||
view2
|
||||
.resizable(axis: .horizontal)
|
||||
}
|
||||
.idealLayout()
|
||||
.frame(width: 200)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 50))
|
||||
)
|
||||
}
|
||||
|
||||
func testFixedSizeViews() {
|
||||
let view1 = ViewWithSize(customSize: CGSize(width: 50, height: 50))
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: 50, height: 50))
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
.idealLayout()
|
||||
|
||||
_ = layout.sizeThatFits(CGSize(width: 300, height: 300))
|
||||
XCTAssertEqual(view1.proposedSizes.count, 1)
|
||||
XCTAssertEqual(view2.proposedSizes.count, 1)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
private class LabelView: UIView {
|
||||
let label = UILabel()
|
||||
|
||||
init(_ text: String) {
|
||||
super.init(frame: .zero)
|
||||
label.text = text
|
||||
label.textColor = .white
|
||||
self.addSubview(label)
|
||||
}
|
||||
|
||||
private lazy var layout: Layout = {
|
||||
HStack {
|
||||
label
|
||||
}
|
||||
}()
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
layout.applyFrame(bounds)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class LayeringServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testLayeringWithAlignment() {
|
||||
|
||||
let label1 = UILabel()
|
||||
label1.text = "Hello World"
|
||||
|
||||
let targetView = UIView()
|
||||
targetView.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view1 = LabelView("1")
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
let view2 = LabelView("2")
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
let view3 = LabelView("3")
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let view4 = LabelView("4")
|
||||
view4.backgroundColor = ColorPallete.orange
|
||||
let view5 = LabelView("5")
|
||||
view5.backgroundColor = ColorPallete.red
|
||||
let view6 = LabelView("6")
|
||||
view6.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view7 = LabelView("7")
|
||||
view7.backgroundColor = ColorPallete.yellow
|
||||
let view8 = LabelView("8")
|
||||
view8.backgroundColor = ColorPallete.orange
|
||||
let view9 = LabelView("9")
|
||||
view9.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = HStack {
|
||||
label1
|
||||
Spacer(8)
|
||||
targetView
|
||||
.frame(width: 100, height: 100)
|
||||
.overlay(alignment: .topLeading) {
|
||||
view1.frame(width: 30, height: 30)
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
view2.frame(width: 30, height: 30)
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
view3.frame(width: 30, height: 30)
|
||||
}
|
||||
.overlay(alignment: .leading) {
|
||||
view4.frame(width: 30, height: 30)
|
||||
}
|
||||
.overlay {
|
||||
view5.frame(width: 30, height: 30)
|
||||
}
|
||||
.overlay(alignment: .trailing) {
|
||||
view6.frame(width: 30, height: 30)
|
||||
}
|
||||
.overlay(alignment: .bottomLeading) {
|
||||
view7.frame(width: 30, height: 30)
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
view8.frame(width: 30, height: 30)
|
||||
}
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
view9.frame(width: 30, height: 30)
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
|
||||
func testLayeringWithNil() {
|
||||
|
||||
let label1 = UILabel()
|
||||
label1.text = "Hello World"
|
||||
|
||||
let targetView = UIView()
|
||||
targetView.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view1: UIView? = nil
|
||||
|
||||
let layout = HStack {
|
||||
label1
|
||||
Spacer(8)
|
||||
targetView
|
||||
.frame(width: 100, height: 100)
|
||||
.overlay(alignment: .topLeading) {
|
||||
view1?.frame(width: 30, height: 30)
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import QuickLayoutBridge
|
||||
import XCTest
|
||||
|
||||
class LayoutDirectionSnapShotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testOverrideLeftToRight() {
|
||||
let label1 = UILabel()
|
||||
let label2 = UILabel()
|
||||
|
||||
label1.text = "Hello"
|
||||
label2.text = "Goodbye"
|
||||
|
||||
let layout = HStack {
|
||||
label1
|
||||
Spacer()
|
||||
label2
|
||||
}
|
||||
.layoutDirection(.leftToRight)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 44))
|
||||
)
|
||||
}
|
||||
|
||||
func testOverrideRightToLeft() {
|
||||
let label1 = UILabel()
|
||||
let label2 = UILabel()
|
||||
|
||||
label1.text = "Hello"
|
||||
label2.text = "Goodbye"
|
||||
|
||||
let layout = HStack {
|
||||
label1
|
||||
Spacer()
|
||||
label2
|
||||
}
|
||||
.layoutDirection(.rightToLeft)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 44))
|
||||
)
|
||||
}
|
||||
|
||||
func testFlexibleFrameAlignment() {
|
||||
let label1 = UILabel()
|
||||
|
||||
label1.text = "Lorem Ipsum"
|
||||
|
||||
let layout =
|
||||
label1
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
.layoutDirection(.rightToLeft)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 44))
|
||||
)
|
||||
}
|
||||
|
||||
func testFixedFrameAlignment() {
|
||||
let label1 = UILabel()
|
||||
|
||||
label1.text = "Lorem Ipsum"
|
||||
|
||||
let layout =
|
||||
label1
|
||||
.frame(width: 200, alignment: .leading)
|
||||
.layoutDirection(.rightToLeft)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 200, height: 44))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class LayoutPriorityServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testWithoutLayoutPriorities() {
|
||||
|
||||
let view1 = UILabel()
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.red
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = HStack(spacing: 8) {
|
||||
view1
|
||||
view2
|
||||
view3
|
||||
view4
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithRedHavingHighestLayoutPriority() {
|
||||
|
||||
let view1 = UILabel()
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.red
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = HStack(spacing: 8) {
|
||||
view1
|
||||
view2
|
||||
.layoutPriority(1)
|
||||
view3
|
||||
view4
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithBlueHavingHighestLayoutPriority() {
|
||||
|
||||
let view1 = UILabel()
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.red
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = HStack(spacing: 8) {
|
||||
view1
|
||||
view2
|
||||
view3
|
||||
.layoutPriority(1)
|
||||
view4
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithOrangeHavingHighestLayoutPriority() {
|
||||
|
||||
let view1 = UILabel()
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.red
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = HStack(spacing: 8) {
|
||||
view1
|
||||
view2
|
||||
view3
|
||||
view4
|
||||
.layoutPriority(1)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithRedAndOrangeHavingHighestLayoutPriority() {
|
||||
|
||||
let view1 = UILabel()
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.red
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = HStack(spacing: 8) {
|
||||
view1
|
||||
view2
|
||||
.layoutPriority(1)
|
||||
view3
|
||||
view4
|
||||
.layoutPriority(1)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithTextHavingHighestLayoutPriority() {
|
||||
|
||||
let view1 = UILabel()
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.red
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = HStack(spacing: 8) {
|
||||
view1
|
||||
.layoutPriority(1)
|
||||
view2
|
||||
view3
|
||||
view4
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 320))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class ListCellItemViewServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTest(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
content: String,
|
||||
time: String,
|
||||
multiline: Bool = false
|
||||
) {
|
||||
|
||||
let titleLabel = UILabel()
|
||||
let subtitlLabel = UILabel()
|
||||
let contentLabel = UILabel()
|
||||
let timeLabel = UILabel()
|
||||
let iconView = UIView()
|
||||
iconView.backgroundColor = .black
|
||||
contentLabel.numberOfLines = 2
|
||||
|
||||
titleLabel.text = title
|
||||
subtitlLabel.text = subtitle
|
||||
contentLabel.text = content
|
||||
timeLabel.text = time
|
||||
|
||||
if multiline {
|
||||
titleLabel.numberOfLines = 0
|
||||
subtitlLabel.numberOfLines = 0
|
||||
contentLabel.numberOfLines = 0
|
||||
timeLabel.numberOfLines = 0
|
||||
}
|
||||
|
||||
let layout = VStack(alignment: .leading) {
|
||||
HStack {
|
||||
titleLabel
|
||||
Spacer()
|
||||
timeLabel
|
||||
Spacer(4)
|
||||
iconView
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
subtitlLabel
|
||||
contentLabel
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 700))
|
||||
)
|
||||
}
|
||||
|
||||
func testShortText() {
|
||||
runTest(
|
||||
title: "Title",
|
||||
subtitle: "Subitle",
|
||||
content: "Content",
|
||||
time: "22:00"
|
||||
)
|
||||
}
|
||||
|
||||
func testLongText() {
|
||||
runTest(
|
||||
title: "Title Nulla non sem et tortor euismod ornare. Duis blandit porta. End!!!",
|
||||
subtitle: "Subtitle Name Aenean nec consectetur massa. Pellentesque id rhoncus metus. Suspendisse tincidunt. End!!!",
|
||||
content: "Content Headline Integer pulvinar mollis ipsum, vel condimentum velit efficitur id. End!!!",
|
||||
time: "22:00"
|
||||
)
|
||||
}
|
||||
|
||||
func testLongTextMultiline() {
|
||||
runTest(
|
||||
title: "Title Nulla non sem et tortor euismod ornare. Duis blandit porta. End!!!",
|
||||
subtitle: "Subtitle Name Aenean nec consectetur massa. Pellentesque id rhoncus metus. Suspendisse tincidunt. End!!!",
|
||||
content: "Content Headline Integer pulvinar mollis ipsum, vel condimentum velit efficitur id. End!!!",
|
||||
time: "22:00",
|
||||
multiline: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class NestedStacksServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTest(on views: (UIView, UIView?)) {
|
||||
takeSnapshot(
|
||||
with:
|
||||
HStack {
|
||||
VStack {
|
||||
VStack {
|
||||
HStack {
|
||||
HStack {
|
||||
views.0
|
||||
if let view = views.1 {
|
||||
view
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
in: .proposed(CGSize(width: 300, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLabelsInHStack() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Label 1"
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "Label 2"
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIViews() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIScrollView() {
|
||||
let view1 = UIScrollView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIScrollView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUITextField() {
|
||||
let view1 = UITextField()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UITextField()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIButton() {
|
||||
let view1 = UIButton(type: .system)
|
||||
view1.setTitle("Button 1", for: .normal)
|
||||
|
||||
let view2 = UIButton(type: .system)
|
||||
view2.setTitle("Button 2", for: .normal)
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testTwoUIImageView() {
|
||||
let view1 = UIImageView(image: FBTestImageGenerator.image(with: ColorPallete.blue, size: CGSize(width: 40, height: 40)))
|
||||
let view2 = UIImageView(image: FBTestImageGenerator.image(with: ColorPallete.yellow, size: CGSize(width: 40, height: 40)))
|
||||
|
||||
runTest(on: (view1, view2))
|
||||
}
|
||||
|
||||
func testSingleLabelInHStack() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Label 1"
|
||||
|
||||
runTest(on: (view1, nil))
|
||||
}
|
||||
|
||||
func testSingleUIView() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
runTest(on: (view1, nil))
|
||||
}
|
||||
|
||||
func testSingleUISlider() {
|
||||
let view1 = UISlider()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
runTest(on: (view1, nil))
|
||||
}
|
||||
|
||||
func testSingleUIScrollView() {
|
||||
let view1 = UIScrollView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
runTest(on: (view1, nil))
|
||||
}
|
||||
|
||||
func testSingleUITextField() {
|
||||
let view1 = UITextField()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
runTest(on: (view1, nil))
|
||||
}
|
||||
|
||||
func testSingleUIButton() {
|
||||
let view1 = UIButton(type: .system)
|
||||
view1.setTitle("Button 1", for: .normal)
|
||||
|
||||
runTest(on: (view1, nil))
|
||||
}
|
||||
|
||||
func testSingleUIImageView() {
|
||||
let view1 = UIImageView(image: FBTestImageGenerator.image(with: ColorPallete.blue, size: CGSize(width: 40, height: 40)))
|
||||
|
||||
runTest(on: (view1, nil))
|
||||
}
|
||||
|
||||
func testResizableView() {
|
||||
let view1 = UIButton(type: .system)
|
||||
view1.setTitle("Button 1", for: .normal)
|
||||
|
||||
takeSnapshot(
|
||||
with:
|
||||
HStack {
|
||||
VStack {
|
||||
VStack {
|
||||
HStack {
|
||||
HStack {
|
||||
view1
|
||||
.resizable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
in: .proposed(CGSize(width: 300, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testResizableViewInFrame() {
|
||||
let view1 = UIButton(type: .system)
|
||||
view1.setTitle("Button 1", for: .normal)
|
||||
|
||||
takeSnapshot(
|
||||
with:
|
||||
HStack {
|
||||
VStack {
|
||||
VStack {
|
||||
HStack {
|
||||
HStack {
|
||||
view1
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
in: .proposed(CGSize(width: 300, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testResizableViewInFrameWithAnotherView() {
|
||||
let view1 = UIButton(type: .system)
|
||||
view1.setTitle("Button 1", for: .normal)
|
||||
|
||||
let view2 = UIButton(type: .system)
|
||||
view2.setTitle("Button 2", for: .normal)
|
||||
|
||||
takeSnapshot(
|
||||
with:
|
||||
HStack {
|
||||
VStack {
|
||||
view1
|
||||
VStack {
|
||||
HStack {
|
||||
EmptyLayout()
|
||||
HStack {
|
||||
view2
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
in: .proposed(CGSize(width: 300, height: 100))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
private final class QuickIntrinsicSizeRangeView: UIView {
|
||||
|
||||
var layout: Layout = EmptyLayout()
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
layout.applyFrame(bounds)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return layout.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class NestedViewsWithIntrinsizeSizeRangeImplementationServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testWithNestedViewsThaContainsLayoutWithHStacks() {
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let parentView1 = QuickIntrinsicSizeRangeView()
|
||||
parentView1.layout = HStack {
|
||||
view1
|
||||
}
|
||||
parentView1.addSubview(view1)
|
||||
|
||||
let label1 = UILabel()
|
||||
label1.text = "Nam id n"
|
||||
label1.numberOfLines = 0
|
||||
|
||||
let parentView2 = QuickIntrinsicSizeRangeView()
|
||||
parentView2.layout = HStack {
|
||||
label1
|
||||
}
|
||||
parentView2.addSubview(label1)
|
||||
|
||||
let layout = HStack {
|
||||
parentView1
|
||||
Spacer(8)
|
||||
parentView2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithNestedViewsThaContainsLayoutWithZStacks() {
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let parentView1 = QuickIntrinsicSizeRangeView()
|
||||
parentView1.layout = ZStack {
|
||||
view1
|
||||
}
|
||||
parentView1.addSubview(view1)
|
||||
|
||||
let label1 = UILabel()
|
||||
label1.text = "Nam id n"
|
||||
label1.numberOfLines = 0
|
||||
|
||||
let parentView2 = QuickIntrinsicSizeRangeView()
|
||||
parentView2.layout = ZStack {
|
||||
label1
|
||||
}
|
||||
parentView2.addSubview(label1)
|
||||
|
||||
let layout = HStack {
|
||||
parentView1
|
||||
Spacer(8)
|
||||
parentView2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithNestedViewsWithHStackAndSpacer() {
|
||||
|
||||
let label1 = UILabel()
|
||||
label1.text = "1"
|
||||
|
||||
let label2 = UILabel()
|
||||
label2.text = "1"
|
||||
|
||||
let parentView1 = QuickIntrinsicSizeRangeView()
|
||||
parentView1.layout = HStack {
|
||||
label1
|
||||
Spacer()
|
||||
label2
|
||||
}
|
||||
parentView1.addSubview(label1)
|
||||
parentView1.addSubview(label2)
|
||||
parentView1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let label3 = UILabel()
|
||||
label3.text = "Nam id n"
|
||||
label3.numberOfLines = 0
|
||||
|
||||
let parentView2 = QuickIntrinsicSizeRangeView()
|
||||
parentView2.layout = HStack {
|
||||
label3
|
||||
}
|
||||
parentView2.addSubview(label3)
|
||||
|
||||
let layout = HStack {
|
||||
parentView1
|
||||
Spacer(8)
|
||||
parentView2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
private final class QuickIntrinsicSizeRangeNotProvidedView: UIView {
|
||||
|
||||
var layout: Layout = EmptyLayout()
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
layout.applyFrame(bounds)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return layout.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class NestedViewsWithoutIntrinsizeSizeRangeImplementationServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
/// The reference layout.
|
||||
func testWithoutNestedViews() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let label1 = UILabel()
|
||||
label1.text = "Nam id n"
|
||||
label1.numberOfLines = 0
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
Spacer(8)
|
||||
label1
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithNestedViewsThaContainsLayoutWithHStacks() {
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let parentView1 = QuickIntrinsicSizeRangeNotProvidedView()
|
||||
parentView1.layout = HStack {
|
||||
view1
|
||||
}
|
||||
parentView1.addSubview(view1)
|
||||
|
||||
let label1 = UILabel()
|
||||
label1.text = "Nam id n"
|
||||
label1.numberOfLines = 0
|
||||
|
||||
let parentView2 = QuickIntrinsicSizeRangeNotProvidedView()
|
||||
parentView2.layout = HStack {
|
||||
label1
|
||||
}
|
||||
parentView2.addSubview(label1)
|
||||
|
||||
let layout = HStack {
|
||||
parentView1
|
||||
Spacer(8)
|
||||
parentView2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithNestedViewsThaContainsLayoutWithZStacks() {
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let parentView1 = QuickIntrinsicSizeRangeNotProvidedView()
|
||||
parentView1.layout = ZStack {
|
||||
view1
|
||||
}
|
||||
parentView1.addSubview(view1)
|
||||
|
||||
let label1 = UILabel()
|
||||
label1.text = "Nam id n"
|
||||
label1.numberOfLines = 0
|
||||
|
||||
let parentView2 = QuickIntrinsicSizeRangeNotProvidedView()
|
||||
parentView2.layout = ZStack {
|
||||
label1
|
||||
}
|
||||
parentView2.addSubview(label1)
|
||||
|
||||
let layout = HStack {
|
||||
parentView1
|
||||
Spacer(8)
|
||||
parentView2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testWithNestedViewsWithHStackAndSpacer() {
|
||||
|
||||
let label1 = UILabel()
|
||||
label1.text = "1"
|
||||
|
||||
let label2 = UILabel()
|
||||
label2.text = "1"
|
||||
|
||||
let parentView1 = QuickIntrinsicSizeRangeNotProvidedView()
|
||||
parentView1.layout = HStack {
|
||||
label1
|
||||
Spacer()
|
||||
label2
|
||||
}
|
||||
parentView1.addSubview(label1)
|
||||
parentView1.addSubview(label2)
|
||||
parentView1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let label3 = UILabel()
|
||||
label3.text = "Nam id n"
|
||||
label3.numberOfLines = 0
|
||||
|
||||
let parentView2 = QuickIntrinsicSizeRangeNotProvidedView()
|
||||
parentView2.layout = HStack {
|
||||
label3
|
||||
}
|
||||
parentView2.addSubview(label3)
|
||||
|
||||
let layout = HStack {
|
||||
parentView1
|
||||
Spacer(8)
|
||||
parentView2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class NewsFeedItemWithFlatHeirarchyViewServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTest(content: TestNewsFeedView.Content) {
|
||||
let view = TestNewsFeedView()
|
||||
view.prepare()
|
||||
view.setContent(content)
|
||||
takeSnapshot(
|
||||
of: view,
|
||||
in: .proposed(CGSize(width: 320, height: 700))
|
||||
)
|
||||
}
|
||||
|
||||
private func runMultiScreenTest(content: TestNewsFeedView.Content, font: (UITraitCollection) -> UIFont) {
|
||||
makeMultipleViewSnapshot(
|
||||
viewFactory: { contentSizeCategory in
|
||||
let traitCollection = UITraitCollection(preferredContentSizeCategory: contentSizeCategory)
|
||||
let view = TestNewsFeedView()
|
||||
view.prepare()
|
||||
view.setContent(content)
|
||||
view.setFont(font(traitCollection))
|
||||
view.layer.borderColor = UIColor.black.cgColor
|
||||
view.layer.borderWidth = 2
|
||||
return view
|
||||
},
|
||||
configuration: SnapshotConfiguration(
|
||||
screenSizes: [.iPhoneSmall, .iPhoneMedium, .iPhoneLarge],
|
||||
preferredContentSizeCategories: [.extraSmall, .small, .medium, .large, .extraExtraExtraLarge],
|
||||
sizingStrategy: [.assign, .measureVerticalIntrinsicSize(clampWithScreenSize: true), .measureVerticalIntrinsicSize(clampWithScreenSize: false)]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testShortText() {
|
||||
let content = TestNewsFeedView.Content(
|
||||
actionText: "Action",
|
||||
posterName: "Poster Name",
|
||||
posterHeadline: "Poster Headline",
|
||||
posterTime: "Poster Time",
|
||||
posterComment: "Poster Comment",
|
||||
contentTitle: "Content Title",
|
||||
contentDomain: "Content Domain",
|
||||
actorComment: "Actor Comment",
|
||||
numberOfLines: 1
|
||||
)
|
||||
runTest(content: content)
|
||||
}
|
||||
|
||||
func testLongText() {
|
||||
let content = TestNewsFeedView.Content(
|
||||
actionText: "Action Nulla non sem et tortor euismod ornare. Duis blandit porta. End!!!",
|
||||
posterName: "Poster Name Aenean nec consectetur massa. Pellentesque id rhoncus metus. Suspendisse tincidunt. End!!!",
|
||||
posterHeadline: "Poster Headline Integer pulvinar mollis ipsum, vel condimentum velit efficitur id. End!!!",
|
||||
posterTime: "Poster Time Morbi laoreet, augue nec consequat elementum, arcu risus facilisis odio. End!!!",
|
||||
posterComment: "Poster Comment Cras vulputate justo arcu, ac varius nunc tempus suscipit. End!!!",
|
||||
contentTitle: "Content Title Nulla sed iaculis libero. End!!!",
|
||||
contentDomain: "Content Domain Nam vitae neque quis mi rhoncus dictum eu ut tellus. End!!!",
|
||||
actorComment: "Actor Comment Suspendisse eget posuere tortor. Pellentesque et mi mauris. Orci varius. End!!!",
|
||||
numberOfLines: 1
|
||||
)
|
||||
runTest(content: content)
|
||||
}
|
||||
|
||||
func testLongTextMultiline() {
|
||||
let content = TestNewsFeedView.Content(
|
||||
actionText: "Action Nulla non sem et tortor euismod ornare. Duis blandit porta. End!!!",
|
||||
posterName: "Poster Aenean nec consectetur massa. Pellentesque id rhoncus metus. Suspendisse tincidunt. End!!!",
|
||||
posterHeadline: "Poster Headline Integer pulvinar mollis ipsum, vel condimentum velit efficitur id. End!!",
|
||||
posterTime: "Poster Time Morbi laoreet, augue nec consequat elementum, arcu risus facilisis odio. End!!!",
|
||||
posterComment: "Poster Comment Cras vulputate justo arcu, ac varius nunc tempus suscipit. End!!!",
|
||||
contentTitle: "Content Title Nulla sed iaculis libero. End!!!",
|
||||
contentDomain: "Content Domain Nam vitae neque quis mi rhoncus dictum eu ut tellus. End!!!",
|
||||
actorComment: "Actor Comment Suspendisse eget posuere tortor. Pellentesque et mi mauris. Orci varius. End!!!",
|
||||
numberOfLines: 0
|
||||
)
|
||||
runTest(content: content)
|
||||
}
|
||||
|
||||
func test_GetStarted() {
|
||||
let content = TestNewsFeedView.Content(
|
||||
actionText: "Action Nulla non sem et tortor euismod ornare. Duis blandit porta. End!!!",
|
||||
posterName: "Poster Aenean nec consectetur massa. Pellentesque id rhoncus metus. Suspendisse tincidunt. End!!!",
|
||||
posterHeadline: "Poster Headline Integer pulvinar mollis ipsum, vel condimentum velit efficitur id. End!!",
|
||||
posterTime: "Poster Time Morbi laoreet, augue nec consequat elementum, arcu risus facilisis odio. End!!!",
|
||||
posterComment: "Poster Comment Cras vulputate justo arcu, ac varius nunc tempus suscipit. End!!!",
|
||||
contentTitle: "Content Title Nulla sed iaculis libero. End!!!",
|
||||
contentDomain: "Content Domain Nam vitae neque quis mi rhoncus dictum eu ut tellus. End!!!",
|
||||
actorComment: "Actor Comment Suspendisse eget posuere tortor. Pellentesque et mi mauris. Orci varius. End!!!",
|
||||
numberOfLines: 0
|
||||
)
|
||||
runMultiScreenTest(
|
||||
content: content,
|
||||
font: { traitCollection in
|
||||
UIFont.preferredFont(forTextStyle: .body, compatibleWith: traitCollection)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private final class TestNewsFeedView: UIView {
|
||||
|
||||
struct Content {
|
||||
let actionText: String
|
||||
let posterName: String
|
||||
let posterHeadline: String
|
||||
let posterTime: String
|
||||
let posterComment: String
|
||||
let contentTitle: String
|
||||
let contentDomain: String
|
||||
let actorComment: String
|
||||
let numberOfLines: Int
|
||||
}
|
||||
|
||||
let actionLabel = UILabel()
|
||||
let optionsLabel = UILabel()
|
||||
let posterImageView = TestPlaceholderView(lineColor: UIColor.black)
|
||||
let posterNameLabel = UILabel()
|
||||
let posterHeadlineLabel = UILabel()
|
||||
let posterTimeLabel = UILabel()
|
||||
let posterCommentLabel = UILabel()
|
||||
let contentImageView = TestPlaceholderView(lineColor: UIColor.black)
|
||||
let contentTitleLabel = UILabel()
|
||||
let contentDomainLabel = UILabel()
|
||||
let likeLabel = UILabel()
|
||||
let commentLabel = UILabel()
|
||||
let shareLabel = UILabel()
|
||||
let actorImageView = TestPlaceholderView(lineColor: UIColor.black)
|
||||
let actorCommentLabel = UILabel()
|
||||
|
||||
func prepare() {
|
||||
self.backgroundColor = .white
|
||||
contentImageView.backgroundColor = .black
|
||||
posterImageView.backgroundColor = .black
|
||||
actorImageView.backgroundColor = .black
|
||||
|
||||
self.addSubview(actionLabel)
|
||||
self.addSubview(optionsLabel)
|
||||
self.addSubview(posterImageView)
|
||||
self.addSubview(posterNameLabel)
|
||||
self.addSubview(posterHeadlineLabel)
|
||||
self.addSubview(posterTimeLabel)
|
||||
self.addSubview(posterCommentLabel)
|
||||
self.addSubview(contentImageView)
|
||||
self.addSubview(contentTitleLabel)
|
||||
self.addSubview(contentDomainLabel)
|
||||
self.addSubview(likeLabel)
|
||||
self.addSubview(commentLabel)
|
||||
self.addSubview(shareLabel)
|
||||
self.addSubview(actorImageView)
|
||||
self.addSubview(actorCommentLabel)
|
||||
}
|
||||
|
||||
func setFont(_ font: UIFont) {
|
||||
actionLabel.font = font
|
||||
posterNameLabel.font = font
|
||||
posterHeadlineLabel.font = font
|
||||
posterTimeLabel.font = font
|
||||
posterCommentLabel.font = font
|
||||
contentTitleLabel.font = font
|
||||
contentDomainLabel.font = font
|
||||
actorCommentLabel.font = font
|
||||
}
|
||||
|
||||
func setContent(_ content: Content) {
|
||||
optionsLabel.text = "..."
|
||||
likeLabel.text = "Like"
|
||||
commentLabel.text = "Comment"
|
||||
shareLabel.text = "Share"
|
||||
|
||||
actionLabel.text = content.actionText
|
||||
posterNameLabel.text = content.posterName
|
||||
posterHeadlineLabel.text = content.posterHeadline
|
||||
posterTimeLabel.text = content.posterTime
|
||||
posterCommentLabel.text = content.posterComment
|
||||
contentTitleLabel.text = content.contentTitle
|
||||
contentDomainLabel.text = content.contentDomain
|
||||
actorCommentLabel.text = content.actorComment
|
||||
|
||||
actionLabel.numberOfLines = content.numberOfLines
|
||||
posterNameLabel.numberOfLines = content.numberOfLines
|
||||
posterHeadlineLabel.numberOfLines = content.numberOfLines
|
||||
posterTimeLabel.numberOfLines = content.numberOfLines
|
||||
posterCommentLabel.numberOfLines = content.numberOfLines
|
||||
contentTitleLabel.numberOfLines = content.numberOfLines
|
||||
contentDomainLabel.numberOfLines = content.numberOfLines
|
||||
actorCommentLabel.numberOfLines = content.numberOfLines
|
||||
}
|
||||
|
||||
@LayoutBuilder
|
||||
func layout() -> Layout {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
HStack {
|
||||
actionLabel
|
||||
Spacer()
|
||||
optionsLabel
|
||||
}
|
||||
|
||||
HStack {
|
||||
posterImageView
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
Spacer(2)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
posterNameLabel
|
||||
posterHeadlineLabel
|
||||
posterTimeLabel
|
||||
}
|
||||
}
|
||||
|
||||
posterCommentLabel
|
||||
contentImageView
|
||||
.resizable()
|
||||
.aspectRatio(CGSize(width: 350, height: 200))
|
||||
|
||||
contentTitleLabel
|
||||
contentDomainLabel
|
||||
HStack {
|
||||
likeLabel
|
||||
Spacer()
|
||||
commentLabel
|
||||
Spacer()
|
||||
shareLabel
|
||||
}
|
||||
HStack {
|
||||
actorImageView
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
Spacer(4)
|
||||
actorCommentLabel
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
layout().applyFrame(bounds)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return layout().sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import QuickLayoutBridge
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class OffsetServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testOffset() {
|
||||
|
||||
let size = 20
|
||||
|
||||
let firstView = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
firstView.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view4 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view4.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view5 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view5.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view6 = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
view6.backgroundColor = ColorPallete.blue
|
||||
|
||||
let lastView = ViewWithSize(customSize: CGSize(width: size, height: size))
|
||||
lastView.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 20) {
|
||||
firstView
|
||||
view2
|
||||
.offset(x: -10, y: 0)
|
||||
view3
|
||||
.offset(x: 10, y: 0)
|
||||
view4
|
||||
.offset(x: 0, y: 10)
|
||||
view5
|
||||
.offset(x: 0, y: -10)
|
||||
view6
|
||||
.offset(x: .infinity, y: .infinity) // should be ignored
|
||||
lastView
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class PaddingServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testPaddingAll() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding(.top, 20)
|
||||
.padding(.leading, 10)
|
||||
.padding(.bottom, 80)
|
||||
.padding(.trailing, 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
|
||||
func testPaddingTop() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding(.top, 20)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
|
||||
func testPaddingLeading() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding(.leading, 20)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
|
||||
func testPaddingTrailing() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding(.trailing, 20)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
|
||||
func testPaddingBottom() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
|
||||
func testPaddingHorizontal() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
|
||||
func testPaddingVertical() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
|
||||
func testPaddingTLTB() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding([.top, .leading, .trailing, .bottom], 20)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
|
||||
func testPaddingLeadingTrailing() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding([.leading, .trailing], 20)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
|
||||
func testPaddingTopBottom() {
|
||||
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
|
||||
let view1 = UIImageView(image: generateTestImage(with: "1", size: size))
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
.padding([.top, .bottom], 20)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200)),
|
||||
containerBackground: ColorPallete.yellow
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,497 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class QuicklyInvalidSizesServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testProposeZero() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize.zero)
|
||||
)
|
||||
}
|
||||
|
||||
func testProposeInfinity() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: CGFloat.infinity, height: .infinity))
|
||||
)
|
||||
}
|
||||
|
||||
func testProposeGreatestFiniteValue() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: CGFloat.greatestFiniteMagnitude, height: .greatestFiniteMagnitude))
|
||||
)
|
||||
}
|
||||
|
||||
func testProposeNan() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: CGFloat.nan, height: .nan))
|
||||
)
|
||||
}
|
||||
|
||||
func testProposeMinusOne() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: -1.0, height: -1.0))
|
||||
)
|
||||
}
|
||||
|
||||
func testProposeMinus100() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: -100.0, height: -100.0))
|
||||
)
|
||||
}
|
||||
|
||||
func testViewReturnMinus1() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = View()
|
||||
view2.mockedSize = CGSize(width: -1.0, height: -1.0)
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 10, height: 10))
|
||||
)
|
||||
}
|
||||
|
||||
func testViewReturnNan() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = View()
|
||||
view2.mockedSize = CGSize(width: CGFloat.nan, height: .nan)
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 10, height: 10))
|
||||
)
|
||||
}
|
||||
|
||||
func testViewReturnInVStack() {
|
||||
let view2 = View()
|
||||
view2.mockedSize = CGSize(width: CGFloat.nan, height: .nan)
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack {
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 10, height: 10))
|
||||
)
|
||||
}
|
||||
|
||||
func testViewReturnsInfinity() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = View()
|
||||
view2.mockedSize = CGSize(width: CGFloat.infinity, height: .infinity)
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 10, height: 10))
|
||||
)
|
||||
}
|
||||
|
||||
func testViewReturns100() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = View()
|
||||
view2.mockedSize = CGSize(width: 100, height: 100)
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = VStack {
|
||||
ZStack {
|
||||
view1
|
||||
.padding(1)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view3
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 10, height: 10))
|
||||
)
|
||||
}
|
||||
|
||||
func testAspectRatio() {
|
||||
let view0 = UIView()
|
||||
view0.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.red
|
||||
|
||||
let view5 = UIView()
|
||||
view5.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view6 = UIView()
|
||||
view6.backgroundColor = ColorPallete.red
|
||||
|
||||
let view7 = UIView()
|
||||
view7.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = VStack {
|
||||
view0
|
||||
.aspectRatio(CGSize(width: 1.0, height: .infinity))
|
||||
.frame(width: 10, height: 10)
|
||||
view1
|
||||
.aspectRatio(CGSize.zero)
|
||||
.frame(width: 10, height: 10)
|
||||
view2
|
||||
.aspectRatio(CGSize(width: -1, height: 0))
|
||||
.frame(width: 10, height: 10)
|
||||
view3
|
||||
.aspectRatio(CGSize(width: 0, height: 1), contentMode: .fill)
|
||||
.frame(width: 10, height: 10)
|
||||
view4
|
||||
.aspectRatio(CGSize(width: 1, height: -1), contentMode: .fill)
|
||||
.frame(width: 10, height: 10)
|
||||
view5
|
||||
.aspectRatio(CGSize(width: 1.0, height: .nan), contentMode: .fill)
|
||||
.frame(width: 10, height: 10)
|
||||
view6
|
||||
.aspectRatio(CGSize(width: 1.0, height: 1.0), contentMode: .fit)
|
||||
.frame(width: 10, height: 0)
|
||||
view7
|
||||
.aspectRatio(CGSize(width: 1.0, height: 1.0), contentMode: .fit)
|
||||
.frame(width: 10, height: .infinity)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 100, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testFrame() {
|
||||
let view0 = UIView()
|
||||
view0.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.red
|
||||
|
||||
let view5 = UIView()
|
||||
view5.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack {
|
||||
view0
|
||||
.frame(width: 10.0, height: .nan)
|
||||
view1
|
||||
.frame(width: .nan, height: 10)
|
||||
view2
|
||||
.frame(width: .infinity, height: 10)
|
||||
view4
|
||||
.frame(width: -10.0, height: 10)
|
||||
view5
|
||||
.frame(width: 10.0, height: -10)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 100, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testPadding() {
|
||||
let view0 = UIView()
|
||||
view0.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack {
|
||||
view0
|
||||
.padding(-10)
|
||||
view1
|
||||
.padding(.infinity)
|
||||
view2
|
||||
.padding(.nan)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 100, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testStacks() {
|
||||
let view0 = UIView()
|
||||
view0.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.red
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.red
|
||||
|
||||
let view5 = UIView()
|
||||
view5.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = VStack {
|
||||
HStack(spacing: .nan) {
|
||||
view0
|
||||
.frame(width: 20, height: 10)
|
||||
view1
|
||||
.frame(width: 20, height: 10)
|
||||
}
|
||||
HStack(spacing: .infinity) {
|
||||
view2
|
||||
.frame(width: 20, height: 10)
|
||||
view3
|
||||
.frame(width: 20, height: 10)
|
||||
}
|
||||
HStack(spacing: -10) {
|
||||
view4
|
||||
.frame(width: 20, height: 10)
|
||||
view5
|
||||
.frame(width: 20, height: 10)
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 100, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testApplyFrameWithNan() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.layer.borderColor = UIColor.black.cgColor
|
||||
view2.layer.borderWidth = 1
|
||||
view2.backgroundColor = .clear
|
||||
|
||||
let layout = ZStack(alignment: .leading) {
|
||||
view1
|
||||
}
|
||||
|
||||
let targetView = UIView()
|
||||
targetView.addSubview(view1)
|
||||
targetView.addSubview(view2)
|
||||
targetView.frame.size = CGSize(width: 200, height: 200)
|
||||
|
||||
layout.applyFrame(CGRect(origin: .zero, size: CGSize(width: CGFloat.nan, height: CGFloat.nan)))
|
||||
|
||||
let backgroundView = UIView()
|
||||
backgroundView.frame.size = CGSize(width: 200, height: 200)
|
||||
backgroundView.backgroundColor = .clear
|
||||
backgroundView.addSubview(targetView)
|
||||
targetView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
|
||||
FBTakeSnapshotOfViewAfterScreenUpdates(backgroundView, nil)
|
||||
}
|
||||
}
|
||||
|
||||
private class View: UIView {
|
||||
|
||||
var mockedSize: CGSize = .zero
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return mockedSize
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import Foundation
|
||||
import QuickLayoutBridge
|
||||
|
||||
struct ColorPallete {
|
||||
static let blue = UIColor(hex: 0x4793AF)
|
||||
static let yellow = UIColor(hex: 0xFFC470)
|
||||
static let orange = UIColor(hex: 0xDD5746)
|
||||
static let red = UIColor(hex: 0x8B322C)
|
||||
}
|
||||
|
||||
enum SizeType {
|
||||
case proposed(_ size: CGSize)
|
||||
case exact(_ size: CGSize)
|
||||
}
|
||||
|
||||
func takeSnapshot(
|
||||
with layout: Layout,
|
||||
in sizeType: SizeType,
|
||||
alignment: Alignment? = nil,
|
||||
identifier: String? = nil,
|
||||
containerBackground: UIColor? = nil,
|
||||
layoutDirection: LayoutDirection? = nil
|
||||
) {
|
||||
let targetView = TestView()
|
||||
targetView.backgroundColor = containerBackground
|
||||
layout.views().forEach { targetView.addSubview($0) }
|
||||
targetView.layout = layout
|
||||
targetView.layoutDirection = layoutDirection
|
||||
targetView.alignment = alignment
|
||||
switch sizeType {
|
||||
case .exact(let size): targetView.frame.size = size
|
||||
case .proposed(let size): targetView.frame.size = targetView.sizeThatFits(size)
|
||||
}
|
||||
|
||||
let backgroundView = UIView()
|
||||
backgroundView.bounds = targetView.bounds
|
||||
backgroundView.backgroundColor = .clear
|
||||
backgroundView.addSubview(targetView)
|
||||
targetView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
|
||||
FBTakeSnapshotOfViewAfterScreenUpdates(backgroundView, nil)
|
||||
}
|
||||
|
||||
func takeSnapshot(of targetView: UIView, in sizeType: SizeType, identifier: String? = nil) {
|
||||
|
||||
switch sizeType {
|
||||
case .exact(let size): targetView.frame.size = size
|
||||
case .proposed(let size): targetView.frame.size = targetView.sizeThatFits(size)
|
||||
}
|
||||
|
||||
let backgroundView = UIView()
|
||||
backgroundView.bounds = targetView.bounds
|
||||
backgroundView.backgroundColor = .clear
|
||||
backgroundView.addSubview(targetView)
|
||||
targetView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
|
||||
FBTakeSnapshotOfViewAfterScreenUpdates(backgroundView, nil)
|
||||
}
|
||||
|
||||
final class TestView: UIView {
|
||||
|
||||
var layout: Layout = EmptyLayout()
|
||||
var alignment: Alignment?
|
||||
var layoutDirection: LayoutDirection?
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if let alignment, let layoutDirection {
|
||||
layout.applyFrame(bounds, alignment: alignment, layoutDirection: layoutDirection)
|
||||
} else if let alignment {
|
||||
layout.applyFrame(bounds, alignment: alignment)
|
||||
} else if let layoutDirection {
|
||||
layout.applyFrame(bounds, alignment: .center, layoutDirection: layoutDirection)
|
||||
} else {
|
||||
layout.applyFrame(bounds)
|
||||
}
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return layout.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
class ViewWithSize: UIView {
|
||||
|
||||
let customSize: CGSize
|
||||
var proposedSizes = [CGSize]()
|
||||
|
||||
init(customSize: CGSize) {
|
||||
self.customSize = customSize
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
proposedSizes.append(size)
|
||||
return customSize
|
||||
}
|
||||
}
|
||||
|
||||
func generateTestImage(with text: String, size: CGSize, backgroundColor: UIColor = ColorPallete.blue) -> UIImage? {
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
let frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||
let label = UILabel(frame: frame)
|
||||
label.textAlignment = .center
|
||||
label.backgroundColor = backgroundColor
|
||||
label.textColor = .white
|
||||
label.font = UIFont.monospacedDigitSystemFont(ofSize: 30, weight: .regular)
|
||||
label.text = text
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(size: frame.size)
|
||||
let image = renderer.image { (context) in
|
||||
label.layer.render(in: context.cgContext)
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
private extension UIColor {
|
||||
convenience init(red: Int, green: Int, blue: Int) {
|
||||
assert(red >= 0 && red <= 255, "Invalid red component")
|
||||
assert(green >= 0 && green <= 255, "Invalid green component")
|
||||
assert(blue >= 0 && blue <= 255, "Invalid blue component")
|
||||
|
||||
self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0)
|
||||
}
|
||||
|
||||
convenience init(hex: Int) {
|
||||
self.init(
|
||||
red: (hex >> 16) & 0xFF,
|
||||
green: (hex >> 8) & 0xFF,
|
||||
blue: hex & 0xFF
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class TestPlaceholderView: UIView {
|
||||
|
||||
private let lineColor: UIColor
|
||||
private let lineWidth: CGFloat
|
||||
private let intrinsicSize: CGSize?
|
||||
private let fillColor: UIColor
|
||||
|
||||
init(lineColor: UIColor = UIColor.black, lineWidth: CGFloat = 2, fillColor: UIColor = .white, intrinsicSize: CGSize? = nil) {
|
||||
self.lineColor = lineColor
|
||||
self.intrinsicSize = intrinsicSize
|
||||
self.lineWidth = lineWidth
|
||||
self.fillColor = fillColor
|
||||
super.init(frame: .zero)
|
||||
layer.borderColor = lineColor.cgColor
|
||||
layer.borderWidth = lineWidth
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// Fully flexible if no intrinsic size is set.
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return intrinsicSize ?? size
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
fillColor.setFill()
|
||||
|
||||
UIRectFill(rect)
|
||||
|
||||
lineColor.setStroke()
|
||||
|
||||
let linePath = UIBezierPath()
|
||||
linePath.lineWidth = lineWidth
|
||||
// First line from top left to bottom right
|
||||
linePath.move(to: CGPoint.zero)
|
||||
linePath.addLine(to: CGPoint(x: rect.width, y: rect.height))
|
||||
linePath.stroke()
|
||||
// Second line from top right to bottom left
|
||||
linePath.move(to: CGPoint(x: rect.width, y: 0))
|
||||
linePath.addLine(to: CGPoint(x: 0, y: rect.height))
|
||||
linePath.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
class ColorView: UIView {
|
||||
|
||||
private let text: String?
|
||||
|
||||
init(_ color: UIColor, text: String? = nil) {
|
||||
self.text = text
|
||||
super.init(frame: .zero)
|
||||
self.backgroundColor = color
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
super.draw(rect)
|
||||
|
||||
if let text {
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = .center
|
||||
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.font: UIFont.systemFont(ofSize: 16),
|
||||
.paragraphStyle: paragraphStyle,
|
||||
.foregroundColor: UIColor.black,
|
||||
]
|
||||
|
||||
let attributedString = NSAttributedString(string: text, attributes: attributes)
|
||||
let textRect = CGRect(x: 0, y: (rect.height - attributedString.size().height) / 2, width: rect.width, height: attributedString.size().height)
|
||||
attributedString.draw(in: textRect)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class BorderView: UIView {
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
self.layer.borderColor = UIColor.black.cgColor
|
||||
self.layer.borderWidth = 1
|
||||
self.backgroundColor = UIColor.clear
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class ResizableServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testResizableWithUIImage() {
|
||||
|
||||
let view1 = UIImageView(image: FBTestImageGenerator.image(with: ColorPallete.blue, size: CGSize(width: 50, height: 50)))
|
||||
let view2 = UIImageView(image: FBTestImageGenerator.image(with: ColorPallete.yellow, size: CGSize(width: 50, height: 50)))
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
view2
|
||||
.resizable()
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testResizableWithUIButton() {
|
||||
|
||||
let view1 = UIButton(type: .system)
|
||||
view1.setTitle("Button 1", for: .normal)
|
||||
view1.setTitleColor(.white, for: .normal)
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIButton(type: .system)
|
||||
view2.setTitle("Button 2", for: .normal)
|
||||
view2.setTitleColor(.white, for: .normal)
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
view2
|
||||
.resizable()
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testResizableWithCustomView() {
|
||||
|
||||
let view1 = ViewWithSize(customSize: CGSize(width: 50, height: 50))
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: 50, height: 50))
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
view2
|
||||
.resizable()
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testResizableWithCustomView_resizeOnlyVertically() {
|
||||
|
||||
let view1 = ViewWithSize(customSize: CGSize(width: 50, height: 50))
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: 50, height: 50))
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
view2
|
||||
.resizable(axis: .vertical)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
|
||||
func testResizableWithCustomView_resizeOnlyHorizontally() {
|
||||
|
||||
let view1 = ViewWithSize(customSize: CGSize(width: 50, height: 50))
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = ViewWithSize(customSize: CGSize(width: 50, height: 30))
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
view2
|
||||
.resizable(axis: .horizontal)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 100))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
private class SingleSubviewView: UIView {
|
||||
|
||||
let label = UILabel()
|
||||
|
||||
init(_ text: String) {
|
||||
super.init(frame: .zero)
|
||||
label.text = text
|
||||
label.textColor = .white
|
||||
self.addSubview(label)
|
||||
}
|
||||
|
||||
@LayoutBuilder
|
||||
var body: any Layout {
|
||||
label
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
body.applyFrame(bounds)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return body.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class SingleViewServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testTestAViewWithSingleSubview() {
|
||||
let view1 = SingleSubviewView("Ut enim dui")
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,574 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class StacksWithSpacingSeverSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testSpacersWithSpacing() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
|
||||
let view4 = UIView()
|
||||
view4.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1
|
||||
.frame(width: 40, height: 40)
|
||||
view2
|
||||
.frame(width: 40, height: 40)
|
||||
Spacer(10)
|
||||
view3
|
||||
.frame(width: 40, height: 40)
|
||||
Spacer(10)
|
||||
view4
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testEmptyLayouts() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testSingleThreeEmptyLayouts() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
HStack(spacing: 10) {
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
}
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testEmptyStack() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
HStack {}
|
||||
VStack {}
|
||||
ZStack {}
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testStacksWithEmptyLayout() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
HStack { EmptyLayout() }
|
||||
HStack { EmptyLayout() }
|
||||
ZStack { EmptyLayout() }
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testStacksWithTwoEmptyLayout() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
VStack {
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
}
|
||||
ZStack {
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
}
|
||||
HStack {
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
}
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testStacksEmptyLayoutAndEmptyStack() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
VStack {
|
||||
EmptyLayout()
|
||||
VStack {}
|
||||
}
|
||||
VStack {
|
||||
EmptyLayout()
|
||||
VStack {}
|
||||
}
|
||||
VStack {
|
||||
EmptyLayout()
|
||||
ZStack {}
|
||||
}
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testStacksEmptyLayoutAndNonEmptyStack() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
VStack {
|
||||
EmptyLayout()
|
||||
HStack {
|
||||
Spacer(10)
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
EmptyLayout()
|
||||
HStack {
|
||||
Spacer(10)
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
EmptyLayout()
|
||||
HStack {
|
||||
Spacer(10)
|
||||
}
|
||||
}
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testEmptyLayoutsAtTheEndAndStart() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
view1.frame(width: 40, height: 40)
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
EmptyLayout()
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testSpacersWithSpacingAtTheEndAndStart() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
Spacer(10)
|
||||
view1
|
||||
.frame(width: 40, height: 40)
|
||||
view2
|
||||
.frame(width: 40, height: 40)
|
||||
Spacer(10)
|
||||
view3
|
||||
.frame(width: 40, height: 40)
|
||||
Spacer(10)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testNestedStackWithZeroSpacer() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer(0)
|
||||
}
|
||||
}
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testStacksWithZeroSpacer() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
Spacer(0)
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testStacksWithZeroSpacerTwice() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
Spacer(0)
|
||||
Spacer(0)
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testStacksWithSpacer10() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 10) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
Spacer(10)
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testStacksWithSpacer30() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 30) {
|
||||
view1.frame(width: 40, height: 40)
|
||||
Spacer(10)
|
||||
Spacer(10)
|
||||
Spacer(10)
|
||||
view2.frame(width: 40, height: 40)
|
||||
view3.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testSpacingWithFullyFlexibleElements() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 30) {
|
||||
view1.frame(height: 40)
|
||||
Spacer(10)
|
||||
Spacer(10)
|
||||
Spacer(10)
|
||||
view2.frame(height: 40)
|
||||
view3.frame(height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testSpacingWithFullyFlexibleElementsWithoutSpacers() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 30) {
|
||||
view1.frame(height: 40)
|
||||
view2.frame(height: 40)
|
||||
view3.frame(height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testSpacingWithFullyFlexibleElementsWithEmptyLayout() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 30) {
|
||||
view1.frame(height: 40)
|
||||
EmptyLayout()
|
||||
view2.frame(height: 40)
|
||||
view3.frame(height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testSpacingWithFullyFlexibleElementsWithEmptyLayoutAndSpacer() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 30) {
|
||||
view1.frame(height: 40)
|
||||
EmptyLayout()
|
||||
Spacer(10)
|
||||
view2.frame(height: 40)
|
||||
view3.frame(height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testSpacingWithFullyFlexibleElementsWithEmptyLayoutAndSpacer2() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 30) {
|
||||
view1.frame(height: 40)
|
||||
Spacer(10)
|
||||
EmptyLayout()
|
||||
view2.frame(height: 40)
|
||||
view3.frame(height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testSpacingWithFullyFlexibleViewsAndSpacer() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(spacing: 30) {
|
||||
view1.frame(height: 40)
|
||||
Spacer(10)
|
||||
Spacer()
|
||||
view2.frame(height: 40)
|
||||
view3.frame(height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutBridge
|
||||
import UIKit
|
||||
|
||||
/// Vertically self-sizing scroll view that grows up to the content height.
|
||||
/// It allows the parent to compress itself below the content's height but never grows larger than the content.
|
||||
/// Same as FOASelfSizingScrollView.
|
||||
public final class TestSelfSizingScrollView: UIScrollView {
|
||||
public enum ScrollableAxis {
|
||||
case vertical, horizontal
|
||||
}
|
||||
|
||||
public var scrollableAxis = ScrollableAxis.vertical
|
||||
public var scrollableContentLayout: (() -> (Element & Layout)?)?
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard let layout = scrollableContentLayout?() else { return }
|
||||
|
||||
switch scrollableAxis {
|
||||
case .vertical:
|
||||
self.contentSize = layout.sizeThatFits(CGSize(width: bounds.width, height: .infinity))
|
||||
layout.applyFrame(CGRect(x: 0, y: 0, width: bounds.width, height: .infinity))
|
||||
case .horizontal:
|
||||
self.contentSize = layout.sizeThatFits(CGSize(width: .infinity, height: bounds.height))
|
||||
layout.applyFrame(CGRect(x: 0, y: 0, width: .infinity, height: bounds.height))
|
||||
}
|
||||
}
|
||||
|
||||
override public func sizeThatFits(_ proposedSize: CGSize) -> CGSize {
|
||||
guard let layout = scrollableContentLayout?() else { return proposedSize }
|
||||
|
||||
switch scrollableAxis {
|
||||
case .vertical:
|
||||
let contentSize = layout.sizeThatFits(CGSize(width: proposedSize.width, height: .infinity))
|
||||
return CGSize(width: contentSize.width, height: min(proposedSize.height, contentSize.height))
|
||||
case .horizontal:
|
||||
let contentSize = layout.sizeThatFits(CGSize(width: .infinity, height: proposedSize.height))
|
||||
return CGSize(width: min(proposedSize.width, contentSize.width), height: contentSize.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import UIKit
|
||||
|
||||
enum ScreenSize {
|
||||
|
||||
/// iPhone SE, 375x667
|
||||
case iPhoneSmall
|
||||
|
||||
/// iPhone 12, 12 Pro, 390x844
|
||||
case iPhoneMedium
|
||||
|
||||
/// iPhone 12 Pro Max, 428x926
|
||||
case iPhoneLarge
|
||||
|
||||
case custom(size: CGSize)
|
||||
|
||||
var size: CGSize {
|
||||
switch self {
|
||||
case .iPhoneSmall:
|
||||
return CGSize(width: 375.0, height: 667.0)
|
||||
case .iPhoneMedium:
|
||||
return CGSize(width: 390.0, height: 844.0)
|
||||
case .iPhoneLarge:
|
||||
return CGSize(width: 428.0, height: 926.0)
|
||||
case .custom(let size):
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
var identifier: String {
|
||||
switch self {
|
||||
case .iPhoneSmall:
|
||||
return "iPhoneSmall"
|
||||
case .iPhoneMedium:
|
||||
return "iPhoneMedium"
|
||||
case .iPhoneLarge:
|
||||
return "iPhoneLarge"
|
||||
case .custom(let size):
|
||||
return "Custom_w_\(size.width)_h_\(size.height)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SizingStrategy {
|
||||
case assign
|
||||
case measureVerticalIntrinsicSize(clampWithScreenSize: Bool)
|
||||
|
||||
func makeIdentifier() -> String {
|
||||
switch self {
|
||||
case .assign: return "SizingStrategy_Assign"
|
||||
case .measureVerticalIntrinsicSize(let clampWithScreenSize):
|
||||
if clampWithScreenSize {
|
||||
return "SizingStrategy_MeasureVerticalIntrinsicSizeWithClampingToScreenSize"
|
||||
} else {
|
||||
return "SizingStrategy_MeasureVerticalIntrinsicSize"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SnapshotConfiguration {
|
||||
let screenSizes: [ScreenSize]
|
||||
let preferredContentSizeCategories: [UIContentSizeCategory]
|
||||
let sizingStrategy: [SizingStrategy]
|
||||
}
|
||||
|
||||
struct FlatSnapshotConfiguration {
|
||||
let screenSize: ScreenSize
|
||||
let preferredContentSizeCategory: UIContentSizeCategory
|
||||
let sizingStrategy: SizingStrategy
|
||||
|
||||
func makeIdentifier() -> String {
|
||||
return "iPhone_Portrait_Orientation_ScreenSize_" + screenSize.identifier + "_" + makeIdentifierFor(contentSizeCategory: preferredContentSizeCategory) + "_" + sizingStrategy.makeIdentifier()
|
||||
}
|
||||
}
|
||||
|
||||
func makeMultipleViewSnapshot<T: UIView>(
|
||||
viewFactory: (UIContentSizeCategory) -> T,
|
||||
configuration: SnapshotConfiguration
|
||||
) {
|
||||
|
||||
let flatConfigurations = configuration.screenSizes.flatMap { screenSize in
|
||||
configuration.preferredContentSizeCategories.flatMap { contentSizeCategory in
|
||||
configuration.sizingStrategy.map { sizingStrategy in
|
||||
return FlatSnapshotConfiguration(
|
||||
screenSize: screenSize,
|
||||
preferredContentSizeCategory: contentSizeCategory,
|
||||
sizingStrategy: sizingStrategy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flatConfigurations.forEach { flatConfiguration in
|
||||
makeSingleViewSnapshot(viewFactory: viewFactory, flatConfiguration: flatConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
func makeSingleViewSnapshot<T: UIView>(
|
||||
viewFactory: (UIContentSizeCategory) -> T,
|
||||
flatConfiguration: FlatSnapshotConfiguration
|
||||
) {
|
||||
let screenSize = flatConfiguration.screenSize
|
||||
let contentSizeCategory = flatConfiguration.preferredContentSizeCategory
|
||||
let sizingStrategy = flatConfiguration.sizingStrategy
|
||||
let identifier = flatConfiguration.makeIdentifier()
|
||||
|
||||
let viewUnderTest = viewFactory(contentSizeCategory)
|
||||
switch sizingStrategy {
|
||||
case .assign:
|
||||
viewUnderTest.frame = CGRect(origin: .zero, size: screenSize.size)
|
||||
case .measureVerticalIntrinsicSize(let clampWithScreenSize):
|
||||
let intrinsicSize = viewUnderTest.sizeThatFits(CGSize(width: screenSize.size.width, height: .infinity))
|
||||
let viewSize = CGSize(width: screenSize.size.width, height: clampWithScreenSize ? min(intrinsicSize.height, screenSize.size.height) : intrinsicSize.height)
|
||||
viewUnderTest.frame = CGRect(origin: .zero, size: viewSize)
|
||||
}
|
||||
|
||||
viewUnderTest.setNeedsLayout()
|
||||
viewUnderTest.layoutIfNeeded()
|
||||
|
||||
if Bundle.main.bundleIdentifier == "com.meta.internal.uipreview" {
|
||||
// let snapshotImage = FBRecordSnapshotWithView(viewUnderTest, true) ?? UIImage()
|
||||
// let imageView = UIImageView(image: snapshotImage)
|
||||
// imageView.backgroundColor = viewUnderTest.backgroundColor
|
||||
// imageView.frame = CGRect(origin: .zero, size: snapshotImage.size)
|
||||
// imageView.contentMode = .scaleAspectFit
|
||||
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.alwaysBounceVertical = true
|
||||
scrollView.alwaysBounceHorizontal = true
|
||||
scrollView.contentSize = viewUnderTest.frame.size
|
||||
scrollView.addSubview(viewUnderTest)
|
||||
FBTakeSnapshotOfViewAfterScreenUpdates(scrollView, "Quickly_MultipleViewTests_" + identifier)
|
||||
} else {
|
||||
FBTakeSnapshotOfViewAfterScreenUpdates(viewUnderTest, "Quickly_MultipleViewTests_" + identifier)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeIdentifierFor(contentSizeCategory: UIContentSizeCategory) -> String {
|
||||
switch contentSizeCategory {
|
||||
case .unspecified: return "UIContentSizeCategory_Unspecified"
|
||||
case .extraSmall: return "UIContentSizeCategory_ExtraSmall"
|
||||
case .small: return "UIContentSizeCategory_Small"
|
||||
case .medium: return "UIContentSizeCategory_Medium"
|
||||
case .large: return "UIContentSizeCategory_Large"
|
||||
case .extraLarge: return "UIContentSizeCategory_ExtraLarge"
|
||||
case .extraExtraLarge: return "UIContentSizeCategory_ExtraExtraLarge"
|
||||
case .extraExtraExtraLarge: return "UIContentSizeCategory_ExtraExtraExtraLarge"
|
||||
case .accessibilityMedium: return "UIContentSizeCategory_AccessibilityMedium"
|
||||
case .accessibilityLarge: return "UIContentSizeCategory_AccessibilityLarge"
|
||||
case .accessibilityExtraLarge: return "UIContentSizeCategory_AccessibilityExtraLarge"
|
||||
case .accessibilityExtraExtraLarge: return "UIContentSizeCategory_AccessibilityExtraExtraLarge"
|
||||
case .accessibilityExtraExtraExtraLarge: return "UIContentSizeCategory_AccessibilityExtraExtraExtraLarge"
|
||||
default: return "UIContentSizeCategory_Unknown"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class UIButtonsServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testButtonsWithDifferentTypes() {
|
||||
|
||||
let button0 = UIButton()
|
||||
button0.setTitleColor(.systemBlue, for: .normal)
|
||||
button0.setTitleColor(.systemBlue.withAlphaComponent(0.7), for: .highlighted)
|
||||
button0.setTitle("Button 0", for: .normal)
|
||||
|
||||
let button1 = UIButton(type: .system)
|
||||
button1.setTitle("Button 1", for: .normal)
|
||||
|
||||
let button2 = UIButton(type: .custom)
|
||||
button2.setTitleColor(.systemBlue, for: .normal)
|
||||
button2.setTitleColor(.systemBlue.withAlphaComponent(0.7), for: .highlighted)
|
||||
button2.setTitle("Button 2", for: .normal)
|
||||
|
||||
let button3 = UIButton(type: .detailDisclosure)
|
||||
button3.setTitle("Button 3", for: .normal)
|
||||
|
||||
let button4 = UIButton(type: .infoDark)
|
||||
button4.setTitle("Button 4", for: .normal)
|
||||
|
||||
let button5 = UIButton(type: .infoLight)
|
||||
button5.setTitle("Button 5", for: .normal)
|
||||
|
||||
let button6 = UIButton(type: .contactAdd)
|
||||
button6.setTitle("Button 6", for: .normal)
|
||||
|
||||
let layout = VStack(spacing: 8) {
|
||||
button0
|
||||
button1
|
||||
button2
|
||||
button3
|
||||
button4
|
||||
button5
|
||||
button6
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testButtonsWithConfigs() {
|
||||
if #available(iOS 15.0, *) {
|
||||
let icon = FBTestImageGenerator.image(with: .systemBlue, size: CGSize(width: 10, height: 10))
|
||||
var button0Config = UIButton.Configuration.plain()
|
||||
button0Config.title = "Button 1"
|
||||
button0Config.subtitle = "Subtitle 1"
|
||||
button0Config.image = icon
|
||||
let button0 = UIButton(configuration: button0Config)
|
||||
|
||||
var button1Config = UIButton.Configuration.plain()
|
||||
button1Config.title = "Button 1"
|
||||
button1Config.subtitle = "Subtitle 1"
|
||||
button1Config.image = icon
|
||||
button1Config.titlePadding = 8
|
||||
button1Config.imagePadding = 4
|
||||
let button1 = UIButton(configuration: button1Config)
|
||||
|
||||
var button2Config = UIButton.Configuration.bordered()
|
||||
button2Config.title = "Button 1"
|
||||
button2Config.subtitle = "Subtitle 1"
|
||||
button2Config.image = icon
|
||||
button2Config.titlePadding = 8
|
||||
button2Config.imagePadding = 4
|
||||
let button2 = UIButton(configuration: button2Config)
|
||||
|
||||
var button3Config = UIButton.Configuration.tinted()
|
||||
button3Config.title = "Button 1"
|
||||
button3Config.subtitle = "Subtitle 1"
|
||||
button3Config.image = icon
|
||||
button3Config.titlePadding = 8
|
||||
button3Config.imagePadding = 4
|
||||
let button3 = UIButton(configuration: button3Config)
|
||||
|
||||
var button4Config = UIButton.Configuration.gray()
|
||||
button4Config.title = "Button 1"
|
||||
button4Config.subtitle = "Subtitle 1"
|
||||
button4Config.image = icon
|
||||
button4Config.titlePadding = 8
|
||||
button4Config.imagePadding = 4
|
||||
let button4 = UIButton(configuration: button4Config)
|
||||
|
||||
var button5Config = UIButton.Configuration.filled()
|
||||
button5Config.title = "Button 1"
|
||||
button5Config.subtitle = "Subtitle 1"
|
||||
button5Config.image = icon
|
||||
button5Config.titlePadding = 8
|
||||
button5Config.imagePadding = 4
|
||||
let button5 = UIButton(configuration: button5Config)
|
||||
|
||||
let layout = VStack(spacing: 8) {
|
||||
button0
|
||||
button1
|
||||
button2
|
||||
button3
|
||||
button4
|
||||
button5
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class UILabelServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testLabelsFirstIsLong() {
|
||||
|
||||
let view1 = UILabel()
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "Lorem Ip"
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testLabelsSecondIsLong() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Lorem Ip"
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view2.numberOfLines = 0
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testLabelsFirstIsLongWithTopAlignment() {
|
||||
|
||||
let view1 = UILabel()
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "Lorem Ip"
|
||||
|
||||
let layout = HStack(alignment: .top) {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLabelsWithCustomFonts() {
|
||||
let view1 = UILabel()
|
||||
view1.font = UIFont.boldSystemFont(ofSize: 20)
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
view1.backgroundColor = ColorPallete.orange
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.font = UIFont.boldSystemFont(ofSize: 30)
|
||||
view2.text = "Lorem Ip"
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(alignment: .top) {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLabelsWithCustomSetWithAttributedString() {
|
||||
let view1 = UILabel()
|
||||
view1.attributedText = NSAttributedString(
|
||||
string: "Mauris ullamcorper lacus eget enim feugiat rhoncus. Nullam vulputate enim ac lorem consequat faucibus.",
|
||||
attributes: [.font: UIFont.boldSystemFont(ofSize: 20)])
|
||||
view1.numberOfLines = 0
|
||||
view1.backgroundColor = ColorPallete.orange
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.attributedText = NSAttributedString(
|
||||
string: "Lorem Ip",
|
||||
attributes: [.font: UIFont.boldSystemFont(ofSize: 30)])
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(alignment: .top) {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLabelsWithCustomFontsWithNewLines() {
|
||||
let view1 = UILabel()
|
||||
view1.font = UIFont.boldSystemFont(ofSize: 20)
|
||||
view1.text = "Mauris ullamcorper lacus eget enim feugiat rhoncus.\n\nNullam vulputate enim ac lorem consequat faucibus."
|
||||
view1.numberOfLines = 0
|
||||
view1.backgroundColor = ColorPallete.orange
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.font = UIFont.boldSystemFont(ofSize: 30)
|
||||
view2.text = "Lorem Ip\n\nEtiam faucibus"
|
||||
view2.numberOfLines = 0
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(alignment: .top) {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLabelsWithCustomSetWithAttributedStringWithNewLines() {
|
||||
let view1 = UILabel()
|
||||
view1.attributedText = NSAttributedString(
|
||||
string: "Mauris ullamcorper lacus eget enim feugiat rhoncus. \n\nNullam vulputate enim ac lorem consequat faucibus.",
|
||||
attributes: [.font: UIFont.boldSystemFont(ofSize: 20)])
|
||||
view1.numberOfLines = 0
|
||||
view1.backgroundColor = ColorPallete.orange
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.attributedText = NSAttributedString(
|
||||
string: "Lorem Ip\n\nEtiam faucibus",
|
||||
attributes: [.font: UIFont.boldSystemFont(ofSize: 30)])
|
||||
view2.numberOfLines = 0
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(alignment: .top) {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLabelsWithCustomFontsAndEmojies() {
|
||||
let view1 = UILabel()
|
||||
view1.font = UIFont.boldSystemFont(ofSize: 20)
|
||||
view1.text = "Mauris ullamcorper 👪 👨👩👦 👨👩👧 👨👩👧👦 👨👩👦👦 👨👩👧👧 👨👨👦"
|
||||
view1.numberOfLines = 0
|
||||
view1.backgroundColor = ColorPallete.orange
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.font = UIFont.boldSystemFont(ofSize: 30)
|
||||
view2.text = "🥳 🙂↕️ 😏 😒"
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(alignment: .top) {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLabelsWithCustomSetWithAttributedStringAndEmojies() {
|
||||
let view1 = UILabel()
|
||||
view1.attributedText = NSAttributedString(
|
||||
string: "Mauris ullamcorper 👪 👨👩👦 👨👩👧\n\n👨👩👧👦 👨👩👦👦 👨👩👧👧 👨👨👦",
|
||||
attributes: [.font: UIFont.boldSystemFont(ofSize: 20)])
|
||||
view1.numberOfLines = 0
|
||||
view1.backgroundColor = ColorPallete.orange
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.numberOfLines = 0
|
||||
view2.attributedText = NSAttributedString(
|
||||
string: "🥳 🙂↕️\n\n😏 😒",
|
||||
attributes: [.font: UIFont.boldSystemFont(ofSize: 30)])
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = HStack(alignment: .top) {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLongLabels() {
|
||||
|
||||
let view1 = UILabel()
|
||||
view1.text = "Vestibulum sit amet magna erat. Nullam sed mi snatch sit amet"
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "Vestibulum sit amet magna erat. Nullam sed mi snatch sit amet"
|
||||
view2.numberOfLines = 0
|
||||
|
||||
let layout = HStack(alignment: .top) {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoLongLabelsWithLayoutPriority() {
|
||||
let view1 = UILabel()
|
||||
view1.text = "Vestibulum sit amet magna erat. Nullam sed mi snatch sit amet"
|
||||
view1.numberOfLines = 0
|
||||
|
||||
let view2 = UILabel()
|
||||
view2.text = "Vestibulum sit amet magna erat. Nullam sed mi snatch sit amet"
|
||||
view2.numberOfLines = 0
|
||||
|
||||
let layout = HStack(alignment: .top) {
|
||||
view1
|
||||
Spacer(8)
|
||||
view2
|
||||
.layoutPriority(1)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 320, height: 480))
|
||||
)
|
||||
}
|
||||
|
||||
func testSizeRoundDown() {
|
||||
let font = UIFont.systemFont(ofSize: 19)
|
||||
let text = "Hello World!\ntwo lines"
|
||||
|
||||
let height = NSAttributedString(string: text, attributes: [.font: font])
|
||||
.boundingRect(with: CGSize(width: 150.0, height: font.lineHeight * 2), options: .usesLineFragmentOrigin, context: nil)
|
||||
.height
|
||||
|
||||
let label = UILabel()
|
||||
label.font = font
|
||||
label.text = text
|
||||
label.numberOfLines = 2
|
||||
label.layer.borderColor = UIColor.systemGray.cgColor
|
||||
label.layer.borderWidth = 1
|
||||
|
||||
let layout = HStack {
|
||||
label
|
||||
.frame(height: height - 0.111)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .exact(CGSize(width: 150, height: 150))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,444 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
final class VFlowServerSnapShotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func buildVFlowSingleChild(
|
||||
itemAlignment: HorizontalAlignment,
|
||||
lineAlignment: VerticalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat
|
||||
) {
|
||||
|
||||
let view = ColorView(ColorPallete.red, text: "1")
|
||||
|
||||
let VFlow = VFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view
|
||||
}
|
||||
takeSnapshot(
|
||||
with: VFlow,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func buildVFlowSingleFixedChild(
|
||||
itemAlignment: HorizontalAlignment,
|
||||
lineAlignment: VerticalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat
|
||||
) {
|
||||
|
||||
let view = ColorView(ColorPallete.red, text: "1")
|
||||
|
||||
let VFlow = VFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view.frame(width: 100, height: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: VFlow,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func buildVflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: HorizontalAlignment, lineAlignment: VerticalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat,
|
||||
layoutDirection: LayoutDirection = .leftToRight
|
||||
) {
|
||||
|
||||
let view1 = ColorView(ColorPallete.red, text: "1")
|
||||
let view2 = ColorView(ColorPallete.orange, text: "2")
|
||||
let view3 = ColorView(ColorPallete.blue, text: "3")
|
||||
|
||||
let VFlow = VFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
view3
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
}
|
||||
.layoutDirection(layoutDirection)
|
||||
|
||||
takeSnapshot(
|
||||
with: VFlow,
|
||||
in: .proposed(CGSize(width: 150, height: 150)),
|
||||
layoutDirection: layoutDirection
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func buildVflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: HorizontalAlignment, lineAlignment: VerticalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat,
|
||||
layoutDirection: LayoutDirection = .leftToRight
|
||||
) {
|
||||
|
||||
let view1 = ColorView(ColorPallete.red, text: "1")
|
||||
let view2 = ColorView(ColorPallete.orange, text: "2")
|
||||
let view3 = ColorView(ColorPallete.blue, text: "3")
|
||||
|
||||
let VFlow = VFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view1
|
||||
.frame(width: 60, height: 50)
|
||||
view2
|
||||
.frame(width: 40, height: 70)
|
||||
view3
|
||||
.frame(width: 50, height: 40)
|
||||
|
||||
}
|
||||
.layoutDirection(layoutDirection)
|
||||
|
||||
takeSnapshot(
|
||||
with: VFlow,
|
||||
in: .proposed(CGSize(width: 150, height: 150)),
|
||||
layoutDirection: layoutDirection
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func buildVflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: HorizontalAlignment, lineAlignment: VerticalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat,
|
||||
layoutDirection: LayoutDirection = .leftToRight
|
||||
) {
|
||||
|
||||
let view1 = ColorView(ColorPallete.red, text: "1")
|
||||
let view2 = ColorView(ColorPallete.orange, text: "2")
|
||||
let view3 = ColorView(ColorPallete.blue, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(.green, text: "5")
|
||||
|
||||
let VFlow = VFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
view3
|
||||
.frame(width: 50, height: 50)
|
||||
view4
|
||||
.frame(width: 50, height: 50)
|
||||
view5
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
.layoutDirection(layoutDirection)
|
||||
|
||||
takeSnapshot(
|
||||
with: VFlow,
|
||||
in: .proposed(CGSize(width: 150, height: 150)),
|
||||
layoutDirection: layoutDirection
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func buildVflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: HorizontalAlignment, lineAlignment: VerticalAlignment,
|
||||
itemSpacing: CGFloat,
|
||||
lineSpacing: CGFloat,
|
||||
layoutDirection: LayoutDirection = .leftToRight
|
||||
) {
|
||||
|
||||
let view1 = ColorView(ColorPallete.red, text: "1")
|
||||
let view2 = ColorView(ColorPallete.orange, text: "2")
|
||||
let view3 = ColorView(ColorPallete.blue, text: "3")
|
||||
let view4 = ColorView(ColorPallete.yellow, text: "4")
|
||||
let view5 = ColorView(.green, text: "5")
|
||||
|
||||
let VFlow = VFlow(itemAlignment: itemAlignment, lineAlignment: lineAlignment, itemSpacing: itemSpacing, lineSpacing: lineSpacing) {
|
||||
view1
|
||||
.frame(width: 80, height: 50)
|
||||
view2
|
||||
.frame(width: 30, height: 70)
|
||||
view3
|
||||
.frame(width: 50, height: 40)
|
||||
view4
|
||||
.frame(width: 50, height: 50)
|
||||
view5
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
}
|
||||
.layoutDirection(layoutDirection)
|
||||
|
||||
takeSnapshot(
|
||||
with: VFlow,
|
||||
in: .proposed(CGSize(width: 150, height: 150)),
|
||||
layoutDirection: layoutDirection
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Single Child
|
||||
func testSingleChild() {
|
||||
buildVFlowSingleChild(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testSingleFixedChild() {
|
||||
buildVFlowSingleFixedChild(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
// MARK: - Multiple Children Same Size Single Line
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLine() {
|
||||
buildVflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLineItemSpacing() {
|
||||
buildVflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLineLineSpacing() {
|
||||
buildVflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 10)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeSingleLineItemAndLineSpacing() {
|
||||
buildVflowMultipleChildrenSameSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 15
|
||||
)
|
||||
}
|
||||
|
||||
// Mark - Multiple Children Different Size Single Line
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLine() {
|
||||
buildVflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemAlignmentLeading() {
|
||||
buildVflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .leading,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemAlignmentTrailing() {
|
||||
buildVflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .trailing,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineAlignmentTop() {
|
||||
buildVflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .top,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineAlignmentBottom() {
|
||||
buildVflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .bottom,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemSpacing() {
|
||||
buildVflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineLineSpacing() {
|
||||
buildVflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 10)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeSingleLineItemAndLineSpacing() {
|
||||
buildVflowMultipleChildrenDifferentSizeSingleLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 15
|
||||
)
|
||||
}
|
||||
|
||||
// Mark - Multiple Children Same Size Multi Line
|
||||
|
||||
func testMultipleChildrenSameSizeMultiLine() {
|
||||
buildVflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeMultiLineItemAlignmentLeading() {
|
||||
buildVflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .leading,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeMultiLineItemAlignmentTrailing() {
|
||||
buildVflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .trailing,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeMultiLineItemSpacing() {
|
||||
buildVflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeMultiLineLineSpacing() {
|
||||
buildVflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 10)
|
||||
}
|
||||
|
||||
func testMultipleChildrenSameSizeMultiLineItemAndLineSpacing() {
|
||||
buildVflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 15
|
||||
)
|
||||
}
|
||||
|
||||
// Mark - Multiple Children Different Size Multi Line
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLine() {
|
||||
buildVflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemAlignmentLeading() {
|
||||
buildVflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .leading,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemAlignmentTrailing() {
|
||||
buildVflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .trailing,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineAlignmentTop() {
|
||||
buildVflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .top,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineAlignmentBottom() {
|
||||
buildVflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .top,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemSpacing() {
|
||||
buildVflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 0)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineLineSpacing() {
|
||||
buildVflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 10)
|
||||
}
|
||||
|
||||
func testMultipleChildrenDifferentSizeMultiLineItemAndLineSpacing() {
|
||||
buildVflowMultipleChildrenDifferentSizeMultiLine(
|
||||
itemAlignment: .center,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 10,
|
||||
lineSpacing: 15
|
||||
)
|
||||
}
|
||||
|
||||
//Mark - RTL
|
||||
|
||||
func testMultipleChildrenItemAlignmentLeadingRTL() {
|
||||
buildVflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .leading,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0,
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
|
||||
func testMultipleChildrenItemAlignmentTrailingRTL() {
|
||||
buildVflowMultipleChildrenSameSizeMultiLine(
|
||||
itemAlignment: .trailing,
|
||||
lineAlignment: .center,
|
||||
itemSpacing: 0,
|
||||
lineSpacing: 0,
|
||||
layoutDirection: .rightToLeft
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class VStackServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
private func runTest(alignment: HorizontalAlignment) {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
|
||||
let layout = VStack(alignment: alignment) {
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
view2
|
||||
.frame(width: 80, height: 80)
|
||||
view3
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testAlignmentCenter() {
|
||||
runTest(alignment: .center)
|
||||
}
|
||||
|
||||
func testAlignmentTop() {
|
||||
runTest(alignment: .leading)
|
||||
}
|
||||
|
||||
func testAlignmentBottom() {
|
||||
runTest(alignment: .trailing)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class ViewTransformServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testCustomAnchorPoints() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
view1.layer.anchorPoint = .zero
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
view1.layer.anchorPoint = CGPoint(x: 1.0, y: 1.0)
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
view3.layer.anchorPoint = CGPoint(x: 0.5, y: 0.2)
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
view3
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testCustomAnchorPointsWithTransform() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
view1.layer.anchorPoint = .zero
|
||||
view1.transform = CGAffineTransformMakeScale(0.9, 0.9)
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
view1.layer.anchorPoint = CGPoint(x: 1.0, y: 1.0)
|
||||
view2.transform = CGAffineTransformMakeScale(0.9, 0.9)
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
view3.transform = CGAffineTransformMakeScale(0.9, 0.9)
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
view3
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testScaleTransform() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
view1.transform = CGAffineTransformMakeScale(0.9, 0.9)
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
view2.transform = CGAffineTransformMakeScale(0.9, 0.9)
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
view3.transform = CGAffineTransformMakeScale(0.9, 0.9)
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
view3
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
|
||||
func testRotateTransform() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
view1.transform = CGAffineTransformMakeRotation(CGFloat.pi / 4)
|
||||
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.yellow
|
||||
view2.transform = CGAffineTransformMakeRotation(CGFloat.pi / 4)
|
||||
|
||||
let view3 = UIView()
|
||||
view3.backgroundColor = ColorPallete.red
|
||||
view3.transform = CGAffineTransformMakeRotation(CGFloat.pi / 4)
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
view3
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 300, height: 300))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class ZStackServerSnaposhTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testSingleUIViewFlexible() {
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
|
||||
func testSingleInflexibleUIView() {
|
||||
let view0 = UIView()
|
||||
view0.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let layout = ZStack {
|
||||
view0
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoUIViewBothFlexible() {
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.yellow
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
view2
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoUIViewOneFlexible() {
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.yellow
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.blue
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
view2
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
|
||||
func runTestWithSingleView(alignment: Alignment) {
|
||||
let view0 = UIView()
|
||||
view0.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let layout = ZStack(alignment: alignment) {
|
||||
view0
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
|
||||
func testTwoInflexibleUIView() {
|
||||
|
||||
let view0 = UIView()
|
||||
view0.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = ZStack {
|
||||
view0
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
|
||||
func runTestWith(alignment: Alignment) {
|
||||
let view0 = UIView()
|
||||
view0.backgroundColor = ColorPallete.yellow
|
||||
|
||||
let view1 = UIView()
|
||||
view1.backgroundColor = ColorPallete.blue
|
||||
let view2 = UIView()
|
||||
view2.backgroundColor = ColorPallete.orange
|
||||
|
||||
let layout = ZStack(alignment: alignment) {
|
||||
view0
|
||||
view1
|
||||
.frame(width: 100, height: 100)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
|
||||
takeSnapshot(
|
||||
with: layout,
|
||||
in: .proposed(CGSize(width: 200, height: 200))
|
||||
)
|
||||
}
|
||||
|
||||
func testCenterAlignment() {
|
||||
runTestWith(alignment: .center)
|
||||
}
|
||||
|
||||
func testTopLeadingAlignment() {
|
||||
runTestWith(alignment: .topLeading)
|
||||
}
|
||||
|
||||
func testTopAlignment() {
|
||||
runTestWith(alignment: .top)
|
||||
}
|
||||
|
||||
func testTopTrailingAlignment() {
|
||||
runTestWith(alignment: .topTrailing)
|
||||
}
|
||||
|
||||
func testLeadingAlignment() {
|
||||
runTestWith(alignment: .leading)
|
||||
}
|
||||
|
||||
func testTrailingAlignment() {
|
||||
runTestWith(alignment: .trailing)
|
||||
}
|
||||
|
||||
func testBottomTrailingAlignment() {
|
||||
runTestWith(alignment: .bottomTrailing)
|
||||
}
|
||||
|
||||
func testBottomLeadingAlignment() {
|
||||
runTestWith(alignment: .bottomLeading)
|
||||
}
|
||||
|
||||
func testBottomAlignment() {
|
||||
runTestWith(alignment: .bottom)
|
||||
}
|
||||
|
||||
func testCenterAlignment_SingleView() {
|
||||
runTestWithSingleView(alignment: .center)
|
||||
}
|
||||
|
||||
func testTopLeadingAlignment_SingleView() {
|
||||
runTestWithSingleView(alignment: .topLeading)
|
||||
}
|
||||
|
||||
func testTopAlignment_SingleView() {
|
||||
runTestWithSingleView(alignment: .top)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import FBServerSnapshotTestCase
|
||||
import FBTestImageGenerator
|
||||
import QuickLayoutBridge
|
||||
|
||||
@MainActor
|
||||
class _QuickLayoutViewImplementationInvalidSizesServerSnapshotTests: FBServerSnapshotTestCase {
|
||||
|
||||
func testProposeInfinity() {
|
||||
let view = TestView1()
|
||||
view.label1.text = "Label 1"
|
||||
view.label2.text = "Label 2"
|
||||
takeSnapshot(
|
||||
of: view,
|
||||
in: .proposed(CGSize(width: 320.0, height: .infinity))
|
||||
)
|
||||
}
|
||||
|
||||
func testProposeGreatestFiniteMagnitude() {
|
||||
let view = TestView1()
|
||||
view.label1.text = "Label 1"
|
||||
view.label2.text = "Label 2"
|
||||
takeSnapshot(
|
||||
of: view,
|
||||
in: .proposed(CGSize(width: 320.0, height: .greatestFiniteMagnitude))
|
||||
)
|
||||
}
|
||||
|
||||
func testProposeNan() {
|
||||
let view = TestView1()
|
||||
view.label1.text = "Label 1"
|
||||
view.label2.text = "Label 2"
|
||||
takeSnapshot(
|
||||
of: view,
|
||||
in: .proposed(CGSize(width: 320.0, height: .nan))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class TestView1: UIView, HasBody {
|
||||
|
||||
let label1 = UILabel()
|
||||
let label2 = UILabel()
|
||||
|
||||
var body: Layout {
|
||||
VStack {
|
||||
label1
|
||||
.padding(10)
|
||||
label2
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
_QuickLayoutViewImplementation.layoutSubviews(self)
|
||||
super.layoutSubviews()
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
_QuickLayoutViewImplementation.sizeThatFits(self, size: size) ?? .zero
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import QuickLayoutBridge
|
||||
@testable import QuickLayoutBridgeTestUsage
|
||||
|
||||
@MainActor
|
||||
final class BodyCreationTests: XCTestCase {
|
||||
|
||||
func testMultipleSizeThatFitsWithoutChangingProps() {
|
||||
let view = BodyCreationTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
let size = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size.width, 33)
|
||||
XCTAssertEqual(size.height, 33)
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
|
||||
let size2 = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size2.width, 33)
|
||||
XCTAssertEqual(size2.height, 33)
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
}
|
||||
|
||||
func testBodyCacheReset() {
|
||||
let view = BodyCreationTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
let size = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size.width, 33)
|
||||
XCTAssertEqual(size.height, 33)
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
|
||||
view.setNeedsLayout()
|
||||
let size2 = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size2.width, 33)
|
||||
XCTAssertEqual(size2.height, 33)
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleSizeThatFitsAndChangingProps() {
|
||||
let view = BodyCreationTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
let size = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size.width, 33)
|
||||
XCTAssertEqual(size.height, 33)
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
|
||||
view.frameSize = CGSize(width: 34, height: 33)
|
||||
let size2 = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size2.width, 34)
|
||||
XCTAssertEqual(size2.height, 33)
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleLayoutSubviewsWithoutChangingProps() {
|
||||
let view = BodyCreationTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
view.layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
|
||||
view.layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleLayoutSubviewsWithChangingProps() {
|
||||
let view = BodyCreationTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
view.layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
|
||||
view.frameSize = CGSize(width: 34, height: 33)
|
||||
view.layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleFlexibilityForAxisCalls() {
|
||||
let view = BodyCreationTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
_ = view.quick_flexibility(for: .horizontal)
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
|
||||
_ = view.quick_flexibility(for: .horizontal)
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import QuickLayoutBridge
|
||||
@testable import QuickLayoutBridgeTestUsage
|
||||
|
||||
@MainActor
|
||||
final class BodyCreationWithNestedViewsTests: XCTestCase {
|
||||
|
||||
func testMultipleSizeThatFitsWithoutChangingProps() {
|
||||
let view = BodyCreationWithNestedViewsTestView()
|
||||
view.childView1.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
view.childView2.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
|
||||
_ = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 1)
|
||||
|
||||
_ = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 4)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 4)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleSizeThatFitsAndChangingProps() {
|
||||
let view = BodyCreationWithNestedViewsTestView()
|
||||
view.childView1.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
view.childView2.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
|
||||
_ = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 1)
|
||||
|
||||
view.property += 1
|
||||
view.childView1.property += 1
|
||||
view.childView2.property += 1
|
||||
|
||||
_ = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 4)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 4)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleLayoutSubviewsWithoutChangingProps() {
|
||||
let view = BodyCreationWithNestedViewsTestView()
|
||||
view.childView1.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
view.childView2.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
|
||||
view.layoutIfNeeded()
|
||||
view.childView1.layoutIfNeeded()
|
||||
view.childView2.layoutIfNeeded()
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 3)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 3)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 2)
|
||||
|
||||
view.layoutIfNeeded()
|
||||
view.childView1.layoutIfNeeded()
|
||||
view.childView2.layoutIfNeeded()
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 3)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 3)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleLayoutSubviewsWithChangingProps() {
|
||||
let view = BodyCreationWithNestedViewsTestView()
|
||||
view.childView1.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
view.childView2.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
|
||||
view.layoutIfNeeded()
|
||||
view.childView1.layoutIfNeeded()
|
||||
view.childView2.layoutIfNeeded()
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 3)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 3)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 2)
|
||||
|
||||
view.property += 1
|
||||
view.childView1.property += 1
|
||||
view.childView2.property += 1
|
||||
|
||||
view.layoutIfNeeded()
|
||||
view.childView1.layoutIfNeeded()
|
||||
view.childView2.layoutIfNeeded()
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 6)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 6)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 4)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 4)
|
||||
}
|
||||
|
||||
func testMultipleFlexibilityForAxisCalls() {
|
||||
let view = BodyCreationWithNestedViewsTestView()
|
||||
view.childView1.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
view.childView2.leafView.frameSize = CGSize(width: 33, height: 33)
|
||||
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
_ = view.quick_flexibility(for: .horizontal)
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 1)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 1)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 0)
|
||||
|
||||
_ = view.quick_flexibility(for: .horizontal)
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView1.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView2.bodyCounter, 2)
|
||||
XCTAssertEqual(view.childView1.leafView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.childView2.leafView.sizeThatFitsCounter, 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutBridge
|
||||
import QuickLayoutCore
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class ComputeGridLayoutSizeThatFitsCountTests: XCTestCase {
|
||||
|
||||
func test_SizeThatFitsInvocationCountWithFuzzyLayoutComparison() {
|
||||
let view1 = TestView()
|
||||
let view2 = TestView()
|
||||
|
||||
view1.sizeThatFitsBlock = { size in
|
||||
return CGSize(width: 150.00000000000007, height: 300)
|
||||
}
|
||||
|
||||
view2.sizeThatFitsBlock = { size in
|
||||
return CGSize(width: 150.00000000000008, height: 300)
|
||||
}
|
||||
|
||||
let view = Grid {
|
||||
|
||||
GridRow {
|
||||
view1
|
||||
view2
|
||||
}
|
||||
}
|
||||
view.sizeThatFits(CGSize(width: 300.0, height: 300.0))
|
||||
|
||||
XCTAssertEqual(view1.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view2.sizeThatFitsCounter, 1)
|
||||
}
|
||||
|
||||
func test_SizeThatFitsInvocationCountWithFixedSizeElements() {
|
||||
|
||||
let view1: TestView = TestView()
|
||||
let view2: TestView = TestView()
|
||||
let view3: TestView = TestView()
|
||||
|
||||
let grid = Grid {
|
||||
GridRow {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
view2
|
||||
.frame(width: 50, height: 50)
|
||||
view3
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
}
|
||||
_ = grid.sizeThatFits(CGSize(width: 300, height: 300))
|
||||
|
||||
XCTAssertEqual(view1.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view2.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view3.sizeThatFitsCounter, 1)
|
||||
|
||||
}
|
||||
|
||||
func test_SizeThatFitsInvocationCountMultiplePartialFlexibilityElements() {
|
||||
let label1 = CountedLabel()
|
||||
label1.text = "Short text"
|
||||
let label2 = CountedLabel()
|
||||
label2.text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
|
||||
let grid = Grid {
|
||||
GridRow {
|
||||
label1
|
||||
label2
|
||||
}
|
||||
}
|
||||
_ = grid.sizeThatFits(CGSize(width: 300, height: 300))
|
||||
XCTAssertEqual(label1.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(label2.sizeThatFitsCounter, 2)
|
||||
}
|
||||
|
||||
func test_SizeThatFitsInvocationCountPartialLargerThanProposed() {
|
||||
|
||||
let view1: TestView = TestView()
|
||||
|
||||
let longLabel: CountedLabel = CountedLabel()
|
||||
longLabel.text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
|
||||
|
||||
let grid = Grid {
|
||||
GridRow {
|
||||
view1
|
||||
.frame(width: 50, height: 50)
|
||||
longLabel
|
||||
}
|
||||
}
|
||||
_ = grid.sizeThatFits(CGSize(width: 300, height: 300))
|
||||
|
||||
XCTAssertEqual(view1.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(longLabel.sizeThatFitsCounter, 2)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutBridge
|
||||
import QuickLayoutCore
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class ComputeStackLayoutSizeThatFitsCountTests: XCTestCase {
|
||||
|
||||
func test_SizeThatFitsInvocationCountWithFuzzyLayoutComparison() {
|
||||
let view1 = TestView()
|
||||
let view2 = TestView()
|
||||
|
||||
view1.sizeThatFitsBlock = { size in
|
||||
return CGSize(width: size.width, height: 150.00000000000007)
|
||||
}
|
||||
|
||||
view2.sizeThatFitsBlock = { size in
|
||||
return CGSize(width: size.width, height: 150.00000000000008)
|
||||
}
|
||||
|
||||
let view = VStack {
|
||||
view1
|
||||
view2
|
||||
}
|
||||
view.sizeThatFits(CGSize(width: 400.0, height: 300.0))
|
||||
|
||||
XCTAssertEqual(view1.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view2.sizeThatFitsCounter, 1)
|
||||
}
|
||||
|
||||
func test_SizeThatFitsInvocationCount_FuzzyComparisonTolerance() {
|
||||
let view1 = TestView()
|
||||
let view2 = TestView()
|
||||
let view3 = TestView()
|
||||
|
||||
view1.sizeThatFitsBlock = { size in
|
||||
return CGSize(width: size.width, height: 100.0001)
|
||||
}
|
||||
|
||||
view2.sizeThatFitsBlock = { size in
|
||||
return CGSize(width: size.width, height: 99.99995)
|
||||
}
|
||||
|
||||
view3.sizeThatFitsBlock = { size in
|
||||
/// This view will be double measured because the resulting layout is crossing the fuzzy comparison threshold.
|
||||
return CGSize(width: size.width, height: 100.0002)
|
||||
}
|
||||
|
||||
let view = VStack {
|
||||
view1
|
||||
view2
|
||||
view3
|
||||
}
|
||||
view.sizeThatFits(CGSize(width: 400.0, height: 300.0))
|
||||
|
||||
XCTAssertEqual(view1.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view2.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view3.sizeThatFitsCounter, 2)
|
||||
}
|
||||
|
||||
func test_SizeThatFitsInvocationCountWithInfinitySize() {
|
||||
let view = FeedItemQuicklyView(frame: .zero)
|
||||
_ = view.sizeThatFits(CGSize(width: 400.0, height: .infinity))
|
||||
|
||||
// Image Views are wrapped in .resizable() modifier,
|
||||
// so the Quickly doesn't call sizeThatFits for them.
|
||||
XCTAssertEqual(view.actorImageView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.contentImageView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.posterImageView.sizeThatFitsCounter, 0)
|
||||
|
||||
XCTAssertEqual(view.actionLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.optionsLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterNameLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterHeadlineLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterTimeLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterCommentLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.contentTitleLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.contentDomainLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.likeLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.commentLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.shareLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.actorCommentLabel.sizeThatFitsCounter, 1)
|
||||
}
|
||||
|
||||
func test_SizeThatFitsInvocationCountWithExactContentSize() {
|
||||
let viewForMeasure = FeedItemQuicklyView(frame: .zero)
|
||||
let size = viewForMeasure.sizeThatFits(CGSize(width: 400.0, height: .infinity))
|
||||
|
||||
let view = FeedItemQuicklyView(frame: .zero)
|
||||
_ = view.sizeThatFits(size)
|
||||
|
||||
// Image Views are wrapped in .resizable() modifier,
|
||||
// so the Quickly doesn't call sizeThatFits for them.
|
||||
XCTAssertEqual(view.actorImageView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.contentImageView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.posterImageView.sizeThatFitsCounter, 0)
|
||||
|
||||
XCTAssertEqual(view.actionLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.optionsLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterNameLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterHeadlineLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterTimeLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterCommentLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.contentTitleLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.contentDomainLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.likeLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.commentLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.shareLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.actorCommentLabel.sizeThatFitsCounter, 1)
|
||||
}
|
||||
|
||||
func test_SizeThatFitsInvocationCountWithSmallerContentSize_40() {
|
||||
let viewForMeasure = FeedItemQuicklyView(frame: .zero)
|
||||
var size = viewForMeasure.sizeThatFits(CGSize(width: 400.0, height: .infinity))
|
||||
size.height -= 40
|
||||
|
||||
let view = FeedItemQuicklyView(frame: .zero)
|
||||
_ = view.sizeThatFits(size)
|
||||
|
||||
// Image Views are wrapped in .resizable() modifier,
|
||||
// so the Quickly doesn't call sizeThatFits for them.
|
||||
XCTAssertEqual(view.actorImageView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.contentImageView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.posterImageView.sizeThatFitsCounter, 0)
|
||||
|
||||
XCTAssertEqual(view.actionLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.optionsLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterNameLabel.sizeThatFitsCounter, 3)
|
||||
XCTAssertEqual(view.posterHeadlineLabel.sizeThatFitsCounter, 3)
|
||||
XCTAssertEqual(view.posterTimeLabel.sizeThatFitsCounter, 3)
|
||||
XCTAssertEqual(view.posterCommentLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.contentTitleLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.contentDomainLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.likeLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.commentLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.shareLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.actorCommentLabel.sizeThatFitsCounter, 2)
|
||||
}
|
||||
|
||||
func test_SizeThatFitsInvocationCountWithHardcodedSize400x400() {
|
||||
|
||||
let view = FeedItemQuicklyView(frame: .zero)
|
||||
_ = view.sizeThatFits(CGSize(width: 400.0, height: 400))
|
||||
|
||||
// Image Views are wrapped in .resizable() modifier,
|
||||
// so the Quickly doesn't call sizeThatFits for them.
|
||||
XCTAssertEqual(view.actorImageView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.contentImageView.sizeThatFitsCounter, 0)
|
||||
XCTAssertEqual(view.posterImageView.sizeThatFitsCounter, 0)
|
||||
|
||||
XCTAssertEqual(view.actionLabel.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.optionsLabel.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.posterNameLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterHeadlineLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterTimeLabel.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.posterCommentLabel.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.contentTitleLabel.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.contentDomainLabel.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.likeLabel.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.commentLabel.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.shareLabel.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.actorCommentLabel.sizeThatFitsCounter, 2)
|
||||
}
|
||||
}
|
||||
|
||||
final class FeedItemQuicklyView: UIView {
|
||||
|
||||
func isAutoLayout() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
let actionLabel = CountedLabel()
|
||||
let optionsLabel = CountedLabel()
|
||||
let posterImageView = FixedSizeViewView(intrinsicSize: CGSize(width: 50, height: 50))
|
||||
let posterNameLabel = CountedLabel()
|
||||
let posterHeadlineLabel = CountedLabel()
|
||||
let posterTimeLabel = CountedLabel()
|
||||
let posterCommentLabel = CountedLabel()
|
||||
let contentImageView = FixedSizeViewView(intrinsicSize: CGSize(width: 350, height: 200))
|
||||
let contentTitleLabel = CountedLabel()
|
||||
let contentDomainLabel = CountedLabel()
|
||||
let likeLabel = CountedLabel()
|
||||
let commentLabel = CountedLabel()
|
||||
let shareLabel = CountedLabel()
|
||||
let actorImageView = FixedSizeViewView(intrinsicSize: CGSize(width: 50, height: 50))
|
||||
let actorCommentLabel = CountedLabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
actionLabel.numberOfLines = 0
|
||||
posterNameLabel.numberOfLines = 0
|
||||
posterHeadlineLabel.numberOfLines = 0
|
||||
posterTimeLabel.numberOfLines = 0
|
||||
posterCommentLabel.numberOfLines = 0
|
||||
contentTitleLabel.numberOfLines = 0
|
||||
contentDomainLabel.numberOfLines = 0
|
||||
actorCommentLabel.numberOfLines = 0
|
||||
prepareViewHierarchy()
|
||||
setData()
|
||||
prepareLayout()
|
||||
}
|
||||
|
||||
func prepareViewHierarchy() {
|
||||
addSubview(actionLabel)
|
||||
addSubview(optionsLabel)
|
||||
addSubview(posterImageView)
|
||||
addSubview(posterNameLabel)
|
||||
addSubview(posterHeadlineLabel)
|
||||
addSubview(posterTimeLabel)
|
||||
addSubview(posterCommentLabel)
|
||||
addSubview(contentImageView)
|
||||
addSubview(contentTitleLabel)
|
||||
addSubview(contentDomainLabel)
|
||||
addSubview(likeLabel)
|
||||
addSubview(commentLabel)
|
||||
addSubview(shareLabel)
|
||||
addSubview(actorImageView)
|
||||
addSubview(actorCommentLabel)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setData() {
|
||||
actionLabel.text = "lorem"
|
||||
posterNameLabel.text = "lorem"
|
||||
posterHeadlineLabel.text = "lorem"
|
||||
posterTimeLabel.text = "lorem"
|
||||
posterCommentLabel.text = "lorem"
|
||||
contentTitleLabel.text = "lorem"
|
||||
contentDomainLabel.text = "lorem"
|
||||
actorCommentLabel.text = "lorem"
|
||||
likeLabel.text = "Like"
|
||||
commentLabel.text = "Comment"
|
||||
shareLabel.text = "Share"
|
||||
optionsLabel.text = "..."
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
private var layout: Layout?
|
||||
|
||||
func prepareLayout() {
|
||||
self.layout = VStack(alignment: .leading) {
|
||||
HStack {
|
||||
actionLabel
|
||||
Spacer()
|
||||
Spacer(8)
|
||||
optionsLabel
|
||||
}
|
||||
|
||||
Spacer(8)
|
||||
HStack(alignment: .top) {
|
||||
posterImageView
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
Spacer(8)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
posterNameLabel
|
||||
Spacer(8)
|
||||
posterHeadlineLabel
|
||||
Spacer(8)
|
||||
posterTimeLabel
|
||||
}
|
||||
}
|
||||
Spacer(8)
|
||||
posterCommentLabel
|
||||
Spacer(8)
|
||||
contentImageView
|
||||
.resizable()
|
||||
.frame(height: 200)
|
||||
Spacer(8)
|
||||
contentTitleLabel
|
||||
Spacer(8)
|
||||
contentDomainLabel
|
||||
Spacer(8)
|
||||
HStack {
|
||||
likeLabel
|
||||
Spacer()
|
||||
commentLabel
|
||||
Spacer()
|
||||
shareLabel
|
||||
}
|
||||
Spacer(8)
|
||||
HStack(alignment: .top) {
|
||||
actorImageView
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
Spacer(8)
|
||||
actorCommentLabel
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
layout?.applyFrame(bounds)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
layout?.sizeThatFits(size) ?? .zero
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import QuickLayoutCore
|
||||
|
||||
final class FuzzyComparisonTests: XCTestCase {
|
||||
|
||||
func testFuzzyComparisons() {
|
||||
|
||||
XCTAssertEqual(Fuzzy.compare(58.666666666666664, greaterThan: 58.66666666666666), false)
|
||||
XCTAssertEqual(Fuzzy.compare(83.66666666666667, greaterThan: 83.66666666666666), false)
|
||||
XCTAssertEqual(Fuzzy.compare(83.66666666666667, greaterThan: 83.66666666666666), false)
|
||||
|
||||
XCTAssertEqual(Fuzzy.compare(58.666666666666664, equalTo: 58.66666666666666), true)
|
||||
XCTAssertEqual(Fuzzy.compare(83.66666666666667, equalTo: 83.66666666666666), true)
|
||||
XCTAssertEqual(Fuzzy.compare(83.66666666666667, equalTo: 83.66666666666666), true)
|
||||
|
||||
// Test isEqual
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, equalTo: 1.0), true)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, equalTo: 1.000001), true)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, equalTo: 2.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.nan, equalTo: Double.nan), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.nan, equalTo: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.nan, equalTo: Double.infinity), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.infinity, equalTo: Double.infinity), true)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.infinity, equalTo: -Double.infinity), false)
|
||||
XCTAssertEqual(Fuzzy.compare(-Double.infinity, equalTo: Double.infinity), false)
|
||||
XCTAssertEqual(Fuzzy.compare(-Double.infinity, equalTo: -Double.infinity), true)
|
||||
XCTAssertEqual(Fuzzy.compare(0.0, equalTo: -0.0), true)
|
||||
|
||||
// Test isLessThan
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, lessThan: 2.0), true)
|
||||
XCTAssertEqual(Fuzzy.compare(2.0, lessThan: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, lessThan: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.nan, lessThan: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.infinity, lessThan: 1.0), false)
|
||||
|
||||
// Test isGreaterThan
|
||||
XCTAssertEqual(Fuzzy.compare(2.0, greaterThan: 1.0), true)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, greaterThan: 2.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, greaterThan: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.nan, greaterThan: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(-Double.infinity, greaterThan: 1.0), false)
|
||||
|
||||
XCTAssertEqual(Fuzzy.compare(Double.infinity, greaterThan: Double.infinity), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.infinity, greaterThan: -Double.infinity), true)
|
||||
XCTAssertEqual(Fuzzy.compare(-Double.infinity, greaterThan: Double.infinity), false)
|
||||
XCTAssertEqual(Fuzzy.compare(-Double.infinity, greaterThan: -Double.infinity), false)
|
||||
|
||||
XCTAssertEqual(Fuzzy.compare(Double.nan, greaterThan: Double.infinity), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.infinity, greaterThan: Double.nan), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.nan, greaterThan: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(10, greaterThan: Double.nan), false)
|
||||
|
||||
// Test isLessThanOrEqual
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, lessThanOrEqual: 2.0), true)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, lessThanOrEqual: 1.0), true)
|
||||
XCTAssertEqual(Fuzzy.compare(2.0, lessThanOrEqual: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.nan, lessThanOrEqual: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.infinity, lessThanOrEqual: 1.0), false)
|
||||
|
||||
// Test isGreaterThanOrEqual
|
||||
XCTAssertEqual(Fuzzy.compare(2.0, greaterThanOrEqual: 1.0), true)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, greaterThanOrEqual: 1.0), true)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, greaterThanOrEqual: 2.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(Double.nan, greaterThanOrEqual: 1.0), false)
|
||||
XCTAssertEqual(Fuzzy.compare(-Double.infinity, greaterThanOrEqual: 1.0), false)
|
||||
}
|
||||
|
||||
func testTolerance() {
|
||||
XCTAssertEqual(Fuzzy.compare(1.0001, equalTo: 1.0, tolerance: 0.0001), true)
|
||||
XCTAssertEqual(Fuzzy.compare(1.00011, equalTo: 1.0, tolerance: 0.0001), false)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, equalTo: 1.0001, tolerance: 0.0001), true)
|
||||
XCTAssertEqual(Fuzzy.compare(1.0, equalTo: 1.00011, tolerance: 0.0001), false)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutBridge
|
||||
import QuickLayoutCore
|
||||
import XCTest
|
||||
|
||||
// Helper to invoke the result builder
|
||||
private func buildLayout(@LayoutBuilder _ builder: () -> Layout) -> Layout {
|
||||
builder()
|
||||
}
|
||||
|
||||
// Helpers to avoid `warning: will never be executed`
|
||||
private func isFalse() -> Bool { false }
|
||||
private func isTrue() -> Bool { true }
|
||||
|
||||
@MainActor
|
||||
final class LayoutBuilderTests: XCTestCase {
|
||||
|
||||
func testReturnValue() {
|
||||
let layout = TestElement()
|
||||
|
||||
let result = buildLayout {
|
||||
layout
|
||||
}
|
||||
|
||||
if let result = result as? TestElement {
|
||||
XCTAssertTrue(result.identifier == layout.identifier)
|
||||
} else {
|
||||
XCTFail("Expected TestElement")
|
||||
}
|
||||
}
|
||||
|
||||
func testReturnOptionalValue() {
|
||||
let layout: Layout? = nil
|
||||
|
||||
let result = buildLayout {
|
||||
layout
|
||||
}
|
||||
|
||||
XCTAssertTrue(result is EmptyElement)
|
||||
}
|
||||
|
||||
func testReturnOptionalValueNonNil() {
|
||||
let expectedLayout = TestElement()
|
||||
let layout: Layout? = expectedLayout
|
||||
|
||||
let result = buildLayout {
|
||||
layout
|
||||
}
|
||||
|
||||
if let result = result as? TestElement {
|
||||
XCTAssertTrue(result.identifier == expectedLayout.identifier)
|
||||
} else {
|
||||
XCTFail("Expected TestElement")
|
||||
}
|
||||
}
|
||||
|
||||
func testReturnViewsWrappedIntoLayout() {
|
||||
let view = UILabel()
|
||||
|
||||
let result = buildLayout {
|
||||
view
|
||||
}
|
||||
|
||||
XCTAssertTrue(result is SingleElement)
|
||||
}
|
||||
|
||||
func testReturnOptionalViewsWrappedIntoLayout() {
|
||||
let view: UILabel? = nil
|
||||
|
||||
let result = buildLayout {
|
||||
view
|
||||
}
|
||||
|
||||
XCTAssertTrue(result is EmptyElement)
|
||||
}
|
||||
|
||||
func testReturnOptionalViewNonNilWrappedIntoLayout() {
|
||||
let view: UILabel? = UILabel()
|
||||
|
||||
let result = buildLayout {
|
||||
view
|
||||
}
|
||||
|
||||
XCTAssertTrue(result is SingleElement)
|
||||
}
|
||||
|
||||
func testOptionalReturnsSharedWhenFalse() {
|
||||
let layout = TestElement()
|
||||
|
||||
let result = buildLayout {
|
||||
if isFalse() {
|
||||
layout
|
||||
}
|
||||
}
|
||||
XCTAssertTrue(result is EmptyElement)
|
||||
}
|
||||
|
||||
func testOptionalReturnsValueWhenTrue() {
|
||||
let layout = TestElement()
|
||||
|
||||
let result = buildLayout {
|
||||
if isTrue() {
|
||||
layout
|
||||
}
|
||||
}
|
||||
if let result = result as? TestElement {
|
||||
XCTAssertTrue(result.identifier == layout.identifier)
|
||||
} else {
|
||||
XCTFail("Expected TestElement")
|
||||
}
|
||||
}
|
||||
|
||||
func testEitherReturnsFirstWhenTrue() {
|
||||
let layout1 = TestElement()
|
||||
let layout2 = TestElement()
|
||||
|
||||
let result = buildLayout {
|
||||
if isTrue() {
|
||||
layout1
|
||||
} else {
|
||||
layout2
|
||||
}
|
||||
}
|
||||
if let result = result as? TestElement {
|
||||
XCTAssertTrue(result.identifier == layout1.identifier)
|
||||
} else {
|
||||
XCTFail("Expected TestElement")
|
||||
}
|
||||
}
|
||||
|
||||
func testEitherReturnsSecondWhenFalse() {
|
||||
let layout1 = TestElement()
|
||||
let layout2 = TestElement()
|
||||
|
||||
let result = buildLayout {
|
||||
if isFalse() {
|
||||
layout1
|
||||
} else {
|
||||
layout2
|
||||
}
|
||||
}
|
||||
if let result = result as? TestElement {
|
||||
XCTAssertTrue(result.identifier == layout2.identifier)
|
||||
} else {
|
||||
XCTFail("Expected TestElement")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TestElement: Layout {
|
||||
|
||||
let identifier: String = UUID().uuidString
|
||||
|
||||
func quick_layoutThatFits(_ proposedSize: CGSize) -> LayoutNode {
|
||||
LayoutNode.empty
|
||||
}
|
||||
|
||||
func quick_flexibility(for axis: Axis) -> Flexibility {
|
||||
.partial
|
||||
}
|
||||
|
||||
func quick_layoutPriority() -> CGFloat {
|
||||
0
|
||||
}
|
||||
|
||||
func quick_extractViewsIntoArray(_ views: inout [UIView]) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import QuickLayoutBridge
|
||||
@testable import QuickLayoutBridgeTestUsage
|
||||
|
||||
@MainActor
|
||||
final class MethodOverrideTests: XCTestCase {
|
||||
func testMultipleLayoutSubviewsWithoutChangingProps() {
|
||||
let view = MethodOverrideTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
|
||||
(view as UIView).layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.layoutSubviewsCounter, 1)
|
||||
|
||||
(view as UIView).layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
XCTAssertEqual(view.layoutSubviewsCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleLayoutSubviewsWithChangingProps() {
|
||||
let view = MethodOverrideTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
|
||||
(view as UIView).layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.layoutSubviewsCounter, 1)
|
||||
|
||||
view.frameSize = CGSize(width: 33, height: 34)
|
||||
|
||||
(view as UIView).layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
XCTAssertEqual(view.layoutSubviewsCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleSizeThatFitsWithoutChangingProps() {
|
||||
let view = MethodOverrideTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
let size = (view as UIView).sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size.width, 100)
|
||||
XCTAssertEqual(size.height, 100)
|
||||
XCTAssertEqual(view.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.bodyCounter, 0)
|
||||
|
||||
let size2 = (view as UIView).sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size2.width, 100)
|
||||
XCTAssertEqual(size2.height, 100)
|
||||
XCTAssertEqual(view.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.bodyCounter, 0)
|
||||
}
|
||||
|
||||
func testMultipleSizeThatFitsAndChangingProps() {
|
||||
let view = MethodOverrideTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
let size = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size.width, 100)
|
||||
XCTAssertEqual(size.height, 100)
|
||||
XCTAssertEqual(view.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.bodyCounter, 0)
|
||||
|
||||
view.frameSize = CGSize(width: 34, height: 33)
|
||||
let size2 = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size2.width, 100)
|
||||
XCTAssertEqual(size2.height, 100)
|
||||
XCTAssertEqual(view.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.bodyCounter, 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import QuickLayoutBridge
|
||||
@testable import QuickLayoutBridgeTestUsage
|
||||
|
||||
@MainActor
|
||||
final class MethodOverrideWithSuperclassTests: XCTestCase {
|
||||
func testMultipleLayoutSubviewsWithoutChangingProps() {
|
||||
let view = MethodOverrideWithSuperclassTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
|
||||
(view as UIView).layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.layoutSubviewsCounter, 1)
|
||||
XCTAssertEqual(view.baseLayoutSubviewsCounter, 1)
|
||||
|
||||
(view as UIView).layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
XCTAssertEqual(view.layoutSubviewsCounter, 2)
|
||||
XCTAssertEqual(view.baseLayoutSubviewsCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleLayoutSubviewsWithChangingProps() {
|
||||
let view = MethodOverrideWithSuperclassTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||
|
||||
(view as UIView).layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 1)
|
||||
XCTAssertEqual(view.layoutSubviewsCounter, 1)
|
||||
XCTAssertEqual(view.baseLayoutSubviewsCounter, 1)
|
||||
|
||||
view.frameSize = CGSize(width: 33, height: 34)
|
||||
|
||||
(view as UIView).layoutSubviews()
|
||||
XCTAssertEqual(view.bodyCounter, 2)
|
||||
XCTAssertEqual(view.layoutSubviewsCounter, 2)
|
||||
XCTAssertEqual(view.baseLayoutSubviewsCounter, 2)
|
||||
}
|
||||
|
||||
func testMultipleSizeThatFitsWithoutChangingProps() {
|
||||
let view = MethodOverrideWithSuperclassTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
let size = (view as UIView).sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size.width, 100)
|
||||
XCTAssertEqual(size.height, 100)
|
||||
XCTAssertEqual(view.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.baseSizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.bodyCounter, 0)
|
||||
|
||||
let size2 = (view as UIView).sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size2.width, 100)
|
||||
XCTAssertEqual(size2.height, 100)
|
||||
XCTAssertEqual(view.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.baseSizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.bodyCounter, 0)
|
||||
}
|
||||
|
||||
func testMultipleSizeThatFitsAndChangingProps() {
|
||||
let view = MethodOverrideWithSuperclassTestView()
|
||||
view.frameSize = CGSize(width: 33, height: 33)
|
||||
let size = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size.width, 100)
|
||||
XCTAssertEqual(size.height, 100)
|
||||
XCTAssertEqual(view.sizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.baseSizeThatFitsCounter, 1)
|
||||
XCTAssertEqual(view.bodyCounter, 0)
|
||||
|
||||
view.frameSize = CGSize(width: 34, height: 33)
|
||||
let size2 = view.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size2.width, 100)
|
||||
XCTAssertEqual(size2.height, 100)
|
||||
XCTAssertEqual(view.sizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.baseSizeThatFitsCounter, 2)
|
||||
XCTAssertEqual(view.bodyCounter, 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import QuickLayoutCore
|
||||
|
||||
private struct TestData {
|
||||
let screenScale: CGFloat
|
||||
let input: CGFloat
|
||||
let output: CGFloat
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class PixelGridRoundingTests: XCTestCase {
|
||||
func test3_0() {
|
||||
let testTada = [
|
||||
TestData(screenScale: 3.0, input: 0.99, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.0, output: 0.0),
|
||||
TestData(screenScale: 3.0, input: 0.49999999, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.333333333, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.6666666666, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.99999999999, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.0333333333333, output: 0.0),
|
||||
|
||||
TestData(screenScale: 3.0, input: 0.00, output: 0.0),
|
||||
|
||||
TestData(screenScale: 3.0, input: 0.11, output: 0.0),
|
||||
TestData(screenScale: 3.0, input: 0.12, output: 0.0),
|
||||
TestData(screenScale: 3.0, input: 0.13, output: 0.0),
|
||||
TestData(screenScale: 3.0, input: 0.14, output: 0.0),
|
||||
TestData(screenScale: 3.0, input: 0.15, output: 0.0),
|
||||
TestData(screenScale: 3.0, input: 0.16, output: 0.0),
|
||||
TestData(screenScale: 3.0, input: 0.17, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.18, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.19, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.20, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.30, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.40, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.41, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.42, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.43, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.44, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.45, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.46, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.47, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.48, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.49, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.50, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.60, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.70, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.80, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.81, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.82, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.83, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.84, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.85, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.86, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.87, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.88, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.89, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.90, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 1.00, output: 1.0),
|
||||
]
|
||||
|
||||
for testData in testTada {
|
||||
for base in 0...10 {
|
||||
let input = CGFloat(base) + testData.input
|
||||
let output = CGFloat(base) + testData.output
|
||||
let result = roundPositionToPixelGrid(input, screenScale: testData.screenScale)
|
||||
XCTAssertEqual(result, output, "input \(input)")
|
||||
|
||||
let result2 = roundPositionToPixelGrid(-input, screenScale: testData.screenScale)
|
||||
XCTAssertEqual(result2, -output, "input \(-input)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test3_0_SizeRounding() {
|
||||
let testTada = [
|
||||
TestData(screenScale: 3.0, input: 0.99, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.0, output: 0.0),
|
||||
TestData(screenScale: 3.0, input: 0.49999999, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.333333333, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.6666666666, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.99999999999, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.0333333333333, output: 0.3333333333333333),
|
||||
|
||||
TestData(screenScale: 3.0, input: 0.00, output: 0.0),
|
||||
|
||||
TestData(screenScale: 3.0, input: 0.11, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.12, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.13, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.14, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.15, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.16, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.17, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.18, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.19, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.20, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.30, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.31, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.32, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.33, output: 0.3333333333333333),
|
||||
TestData(screenScale: 3.0, input: 0.34, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.35, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.36, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.37, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.38, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.39, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.40, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.41, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.42, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.43, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.44, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.45, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.46, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.47, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.48, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.49, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.50, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.60, output: 0.6666666666666666),
|
||||
TestData(screenScale: 3.0, input: 0.70, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.80, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.81, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.82, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.83, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.84, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.85, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.86, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.87, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.88, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.89, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 0.90, output: 1.0),
|
||||
TestData(screenScale: 3.0, input: 1.00, output: 1.0),
|
||||
]
|
||||
|
||||
for testData in testTada {
|
||||
for base in 0...10 {
|
||||
let input = CGFloat(base) + testData.input
|
||||
let output = CGFloat(base) + testData.output
|
||||
let result = roundSizeToPixelGrid(input, screenScale: testData.screenScale)
|
||||
XCTAssertEqual(result, output, "input \(input)")
|
||||
|
||||
let result2 = roundSizeToPixelGrid(-input, screenScale: testData.screenScale)
|
||||
XCTAssertEqual(result2, -output, "input \(-input)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test2_0() {
|
||||
let testTada = [
|
||||
TestData(screenScale: 2.0, input: 0.99, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.0, output: 0.0),
|
||||
TestData(screenScale: 2.0, input: 0.49999999, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.75, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.74, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.25, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.56666666666, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.57777777777, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.99219191, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.99999999, output: 1.0),
|
||||
|
||||
TestData(screenScale: 2.0, input: 0.0, output: 0.0),
|
||||
TestData(screenScale: 2.0, input: 0.1, output: 0.0),
|
||||
TestData(screenScale: 2.0, input: 0.2, output: 0.0),
|
||||
TestData(screenScale: 2.0, input: 0.24999, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.25, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.3, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.4, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.5, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.6, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.7, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.8, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.9, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 1.0, output: 1.0),
|
||||
]
|
||||
|
||||
for testData in testTada {
|
||||
for base in 0...10 {
|
||||
let input = CGFloat(base) + testData.input
|
||||
let output = CGFloat(base) + testData.output
|
||||
let result = roundPositionToPixelGrid(input, screenScale: testData.screenScale)
|
||||
XCTAssertEqual(result, output, "input \(input)")
|
||||
|
||||
let result2 = roundPositionToPixelGrid(-input, screenScale: testData.screenScale)
|
||||
XCTAssertEqual(result2, -output, "input \(-input)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func test2_0_SizeRounding() {
|
||||
let testTada = [
|
||||
TestData(screenScale: 2.0, input: 0.99, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.0, output: 0.0),
|
||||
TestData(screenScale: 2.0, input: 0.49999999, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.75, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.74, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.25, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.56666666666, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.57777777777, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.99219191, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.99999999, output: 1.0),
|
||||
|
||||
TestData(screenScale: 2.0, input: 0.0, output: 0.0),
|
||||
TestData(screenScale: 2.0, input: 0.1, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.2, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.24999, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.25, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.3, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.4, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.5, output: 0.5),
|
||||
TestData(screenScale: 2.0, input: 0.6, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.7, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.8, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 0.9, output: 1.0),
|
||||
TestData(screenScale: 2.0, input: 1.0, output: 1.0),
|
||||
]
|
||||
|
||||
for testData in testTada {
|
||||
for base in 0...10 {
|
||||
let input = CGFloat(base) + testData.input
|
||||
let output = CGFloat(base) + testData.output
|
||||
let result = roundSizeToPixelGrid(input, screenScale: testData.screenScale)
|
||||
XCTAssertEqual(result, output, "input \(input)")
|
||||
|
||||
let result2 = roundSizeToPixelGrid(-input, screenScale: testData.screenScale)
|
||||
XCTAssertEqual(result2, -output, "input \(-input)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSpecialRoundingLogic() {
|
||||
var result: CGFloat = 0.0
|
||||
|
||||
result = roundPositionToPixelGrid(1.249999, screenScale: 2.0)
|
||||
XCTAssertEqual(result, 1.5)
|
||||
|
||||
result = roundPositionToPixelGrid(-1.249999, screenScale: 2.0)
|
||||
XCTAssertEqual(result, -1.5)
|
||||
}
|
||||
|
||||
func testNegativeValues() {
|
||||
var result: CGFloat = 0.0
|
||||
result = roundPositionToPixelGrid(-0.4, screenScale: 2.0)
|
||||
XCTAssertEqual(result, -0.5)
|
||||
|
||||
result = roundPositionToPixelGrid(-1.56666666666, screenScale: 2.0)
|
||||
XCTAssertEqual(result, -1.5)
|
||||
|
||||
result = roundPositionToPixelGrid(-2.00000, screenScale: 3.0)
|
||||
XCTAssertEqual(result, -2.0)
|
||||
}
|
||||
|
||||
func testInvalidValues() {
|
||||
var result: CGFloat = 0.0
|
||||
|
||||
result = roundPositionToPixelGrid(CGFloat.nan, screenScale: 2.0)
|
||||
XCTAssertTrue(result.isNaN)
|
||||
|
||||
result = roundPositionToPixelGrid(CGFloat.greatestFiniteMagnitude - 10, screenScale: 2.0)
|
||||
XCTAssertEqual(result, CGFloat.greatestFiniteMagnitude - 10)
|
||||
|
||||
result = roundPositionToPixelGrid(CGFloat.greatestFiniteMagnitude, screenScale: 2.0)
|
||||
XCTAssertEqual(result, CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
result = roundPositionToPixelGrid(CGFloat.infinity, screenScale: 2.0)
|
||||
XCTAssertEqual(result, .infinity)
|
||||
|
||||
result = roundPositionToPixelGrid(CGFloat.nan, screenScale: 3.0)
|
||||
XCTAssertTrue(result.isNaN)
|
||||
|
||||
result = roundPositionToPixelGrid(CGFloat.greatestFiniteMagnitude - 10, screenScale: 3.0)
|
||||
XCTAssertEqual(result, CGFloat.greatestFiniteMagnitude - 10)
|
||||
|
||||
result = roundPositionToPixelGrid(CGFloat.infinity, screenScale: 3.0)
|
||||
XCTAssertEqual(result, .infinity)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutBridge
|
||||
import QuickLayoutCore
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class FixedSizeViewView: UIView {
|
||||
|
||||
var sizeThatFitsCounter = 0
|
||||
private let intrinsicSize: CGSize?
|
||||
|
||||
init(intrinsicSize: CGSize? = nil) {
|
||||
self.intrinsicSize = intrinsicSize
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// Fully flexible if no intrinsic size is set.
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCounter += 1
|
||||
return intrinsicSize ?? size
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return intrinsicSize ?? .zero
|
||||
}
|
||||
}
|
||||
|
||||
class CountedLabel: UILabel {
|
||||
var sizeThatFitsCounter = 0
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
|
||||
final class TestView: UIView {
|
||||
|
||||
var sizeThatFitsCounter = 0
|
||||
var sizeThatFitsBlock: ((CGSize) -> CGSize)?
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCounter += 1
|
||||
return sizeThatFitsBlock?(size) ?? .zero
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import QuickLayoutBridge
|
||||
@testable @preconcurrency import QuickLayoutCore
|
||||
|
||||
@MainActor
|
||||
class ThreadSafetyTests: XCTestCase {
|
||||
|
||||
func testSpacerInASingleStack() {
|
||||
let layout1 = HStack {
|
||||
Spacer(10)
|
||||
}
|
||||
let size1 = layout1.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size1.height, 0)
|
||||
XCTAssertEqual(size1.width, 10)
|
||||
|
||||
let layout2 = VStack {
|
||||
Spacer(10)
|
||||
}
|
||||
let size2 = layout2.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size2.height, 10)
|
||||
XCTAssertEqual(size2.width, 0)
|
||||
}
|
||||
|
||||
func testSpacerInANestedStacks() {
|
||||
let layout1 = VStack {
|
||||
HStack {
|
||||
Spacer(10)
|
||||
}
|
||||
}
|
||||
let size1 = layout1.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size1.height, 0)
|
||||
XCTAssertEqual(size1.width, 10)
|
||||
|
||||
let layout2 = HStack {
|
||||
VStack {
|
||||
Spacer(10)
|
||||
}
|
||||
}
|
||||
let size2 = layout2.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size2.height, 10)
|
||||
XCTAssertEqual(size2.width, 0)
|
||||
}
|
||||
|
||||
func testNestedStacks() {
|
||||
let element1 = TestElement()
|
||||
let element2 = TestElement()
|
||||
let element3 = TestElement()
|
||||
let element4 = TestElement()
|
||||
|
||||
let layout1 =
|
||||
HStack {
|
||||
element1
|
||||
VStack {
|
||||
element2
|
||||
HStack {
|
||||
element3
|
||||
VStack {
|
||||
element4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let size1 = layout1.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
XCTAssertEqual(size1.height, 0)
|
||||
XCTAssertEqual(size1.width, 0)
|
||||
XCTAssertEqual(element1.layoutMainAxis, [.horizontal])
|
||||
XCTAssertEqual(element1.flexibilityMainAxis, [.horizontal])
|
||||
XCTAssertEqual(element2.layoutMainAxis, [.vertical])
|
||||
XCTAssertEqual(element2.flexibilityMainAxis, [.vertical])
|
||||
XCTAssertEqual(element3.layoutMainAxis, [.horizontal])
|
||||
XCTAssertEqual(element3.flexibilityMainAxis, [.horizontal])
|
||||
XCTAssertEqual(element4.layoutMainAxis, [.vertical])
|
||||
XCTAssertEqual(element4.flexibilityMainAxis, [.vertical])
|
||||
}
|
||||
|
||||
func testNestedStacksOnBackgroundQueue() {
|
||||
let element1 = TestElement()
|
||||
let element2 = TestElement()
|
||||
let element3 = TestElement()
|
||||
let element4 = TestElement()
|
||||
let layout1 =
|
||||
HStack {
|
||||
element1
|
||||
VStack {
|
||||
element2
|
||||
HStack {
|
||||
element3
|
||||
VStack {
|
||||
element4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let expectation = XCTestExpectation(description: "Layout on background queue")
|
||||
let queue = DispatchQueue(label: "com.quick_layout.test_queue")
|
||||
queue.async {
|
||||
_ = layout1.sizeThatFits(CGSize(width: 100, height: 100))
|
||||
expectation.fulfill()
|
||||
}
|
||||
wait(for: [expectation], timeout: 15.0)
|
||||
|
||||
XCTAssertEqual(element1.layoutMainAxis, [.horizontal])
|
||||
XCTAssertEqual(element1.flexibilityMainAxis, [.horizontal])
|
||||
XCTAssertEqual(element2.layoutMainAxis, [.vertical])
|
||||
XCTAssertEqual(element2.flexibilityMainAxis, [.vertical])
|
||||
XCTAssertEqual(element3.layoutMainAxis, [.horizontal])
|
||||
XCTAssertEqual(element3.flexibilityMainAxis, [.horizontal])
|
||||
XCTAssertEqual(element4.layoutMainAxis, [.vertical])
|
||||
XCTAssertEqual(element4.flexibilityMainAxis, [.vertical])
|
||||
}
|
||||
}
|
||||
|
||||
private class TestElement: Element {
|
||||
|
||||
var layoutMainAxis: Set<Axis> = []
|
||||
var flexibilityMainAxis: Set<Axis> = []
|
||||
|
||||
func quickInternal_isSpacer() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func quick_flexibility(for axis: Axis) -> Flexibility {
|
||||
let mainAxis = LayoutContext.latestMainAxis
|
||||
flexibilityMainAxis.insert(mainAxis)
|
||||
return .fixedSize
|
||||
}
|
||||
|
||||
func quick_layoutPriority() -> CGFloat {
|
||||
0
|
||||
}
|
||||
|
||||
func quick_layoutThatFits(_ proposedSize: CGSize) -> LayoutNode {
|
||||
let mainAxis = LayoutContext.latestMainAxis
|
||||
layoutMainAxis.insert(mainAxis)
|
||||
return LayoutNode(view: nil, dimensions: ElementDimensions(CGSize.zero))
|
||||
}
|
||||
|
||||
func quick_extractViewsIntoArray(_ views: inout [UIView]) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,495 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import QuickLayoutCore
|
||||
|
||||
@MainActor
|
||||
class UIKitStandardLibrarySizingBehaviourTests: XCTestCase {
|
||||
|
||||
/// UIButton behaves as a fixed size view, but returns partial flexibility.
|
||||
private func runTestForButton(_ view: UIButton, name: String) {
|
||||
|
||||
let expectedSize = view.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
let sizeFor0x0 = view.quick_layoutThatFits(.zero).size
|
||||
let sizeFor1x1 = view.quick_layoutThatFits(CGSize(width: 1, height: 1)).size
|
||||
let sizeFor20x10 = view.quick_layoutThatFits(CGSize(width: 20, height: 10)).size
|
||||
let sizeFor320x480 = view.quick_layoutThatFits(CGSize(width: 320, height: 480)).size
|
||||
let sizeForCGFloatMax = view.quick_layoutThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).size
|
||||
|
||||
XCTAssertEqual(sizeFor0x0, expectedSize, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor1x1, expectedSize, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor20x10, expectedSize, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor320x480, expectedSize, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeForCGFloatMax, expectedSize, "Size doesn't match for \(name)")
|
||||
|
||||
let flexibilityX = view.quick_flexibility(for: .horizontal)
|
||||
let flexibilityY = view.quick_flexibility(for: .vertical)
|
||||
XCTAssertEqual(Flexibility.partial, flexibilityX, "Flexibility doesn't match for \(name)")
|
||||
XCTAssertEqual(Flexibility.partial, flexibilityY, "Flexibility doesn't match for \(name)")
|
||||
}
|
||||
|
||||
private func runTestForLabel(_ view: UILabel, name: String) {
|
||||
view.numberOfLines = 1
|
||||
|
||||
let expectedSize = view.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
var sizeFor0x0 = view.quick_layoutThatFits(.zero).size
|
||||
var sizeFor1x1 = view.quick_layoutThatFits(CGSize(width: 1, height: 1)).size
|
||||
var sizeFor20x10 = view.quick_layoutThatFits(CGSize(width: 20, height: 10)).size
|
||||
var sizeFor320x480 = view.quick_layoutThatFits(CGSize(width: 320, height: 480)).size
|
||||
var sizeForCGFloatMax = view.quick_layoutThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).size
|
||||
|
||||
XCTAssertEqual(sizeFor0x0, CGSize.zero, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor1x1, CGSize.zero, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor20x10, CGSize(width: 20, height: 10), "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor320x480, expectedSize, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeForCGFloatMax, expectedSize, "Size doesn't match for \(name)")
|
||||
|
||||
var flexibilityX = view.quick_flexibility(for: .horizontal)
|
||||
var flexibilityY = view.quick_flexibility(for: .vertical)
|
||||
XCTAssertEqual(Flexibility.partial, flexibilityX, "Flexibility doesn't match for \(name)")
|
||||
XCTAssertEqual(Flexibility.partial, flexibilityY, "Flexibility doesn't match for \(name)")
|
||||
|
||||
view.numberOfLines = 0
|
||||
|
||||
sizeFor0x0 = view.quick_layoutThatFits(.zero).size
|
||||
sizeFor1x1 = view.quick_layoutThatFits(CGSize(width: 1, height: 1)).size
|
||||
sizeFor20x10 = view.quick_layoutThatFits(CGSize(width: 20, height: 1000)).size
|
||||
sizeFor320x480 = view.quick_layoutThatFits(CGSize(width: 320, height: 480)).size
|
||||
sizeForCGFloatMax = view.quick_layoutThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).size
|
||||
|
||||
XCTAssertEqual(sizeFor0x0, CGSize.zero, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor1x1, CGSize.zero, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor20x10, view.sizeThatFits(CGSize(width: 20, height: 1000)), "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor320x480, expectedSize, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeForCGFloatMax, expectedSize, "Size doesn't match for \(name)")
|
||||
|
||||
flexibilityX = view.quick_flexibility(for: .horizontal)
|
||||
flexibilityY = view.quick_flexibility(for: .vertical)
|
||||
XCTAssertEqual(Flexibility.partial, flexibilityX, "Flexibility doesn't match for \(name)")
|
||||
XCTAssertEqual(Flexibility.partial, flexibilityY, "Flexibility doesn't match for \(name)")
|
||||
}
|
||||
|
||||
private func runTestForFullyFlexibleView(_ view: UIView, name: String, flexibility: Flexibility = .fullyFlexible) {
|
||||
let sizeFor0x0 = view.quick_layoutThatFits(.zero).size
|
||||
let sizeFor1x1 = view.quick_layoutThatFits(CGSize(width: 1, height: 1)).size
|
||||
let sizeFor320x480 = view.quick_layoutThatFits(CGSize(width: 320, height: 480)).size
|
||||
let sizeForCGFloatMax = view.quick_layoutThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).size
|
||||
|
||||
XCTAssertEqual(sizeFor0x0, CGSize.zero, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor1x1, CGSize(width: 1, height: 1), "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor320x480, CGSize(width: 320, height: 480), "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeForCGFloatMax, CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), "Size doesn't match for \(name)")
|
||||
|
||||
let flexibilityX = view.quick_flexibility(for: .horizontal)
|
||||
let flexibilityY = view.quick_flexibility(for: .vertical)
|
||||
XCTAssertEqual(flexibility, flexibilityX, "Flexibility doesn't match for \(name)")
|
||||
XCTAssertEqual(flexibility, flexibilityY, "Flexibility doesn't match for \(name)")
|
||||
}
|
||||
|
||||
private func runTestForFixedSizeViews(_ view: UIView, exectedSize: CGSize, name: String) {
|
||||
let sizeFor0x0 = view.quick_layoutThatFits(.zero).size
|
||||
let sizeFor1x1 = view.quick_layoutThatFits(CGSize(width: 1, height: 1)).size
|
||||
let sizeFor320x480 = view.quick_layoutThatFits(CGSize(width: 320, height: 480)).size
|
||||
let sizeForCGFloatMax = view.quick_layoutThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).size
|
||||
|
||||
XCTAssertEqual(sizeFor0x0, exectedSize, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor1x1, exectedSize, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor320x480, exectedSize, "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeForCGFloatMax, exectedSize, "Size doesn't match for \(name)")
|
||||
|
||||
let flexibilityX = view.quick_flexibility(for: .horizontal)
|
||||
let flexibilityY = view.quick_flexibility(for: .vertical)
|
||||
XCTAssertEqual(Flexibility.fixedSize, flexibilityX, "Flexibility doesn't match for \(name)")
|
||||
XCTAssertEqual(Flexibility.fixedSize, flexibilityY, "Flexibility doesn't match for \(name)")
|
||||
}
|
||||
|
||||
private func runTestForHorizontallyExpandableView(_ view: UIView, height: CGFloat, name: String) {
|
||||
let sizeFor0x0 = view.quick_layoutThatFits(.zero).size
|
||||
let sizeFor1x1 = view.quick_layoutThatFits(CGSize(width: 1, height: 1)).size
|
||||
let sizeFor320x480 = view.quick_layoutThatFits(CGSize(width: 320, height: 480)).size
|
||||
let sizeForCGFloatMax = view.quick_layoutThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).size
|
||||
|
||||
XCTAssertEqual(sizeFor0x0, CGSize(width: 0, height: height), "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor1x1, CGSize(width: 1, height: height), "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeFor320x480, CGSize(width: 320, height: height), "Size doesn't match for \(name)")
|
||||
XCTAssertEqual(sizeForCGFloatMax, CGSize(width: CGFloat.greatestFiniteMagnitude, height: height), "Size doesn't match for \(name)")
|
||||
|
||||
let flexibilityX = view.quick_flexibility(for: .horizontal)
|
||||
let flexibilityY = view.quick_flexibility(for: .vertical)
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, flexibilityX, "Flexibility doesn't match for \(name)")
|
||||
XCTAssertEqual(Flexibility.fixedSize, flexibilityY, "Flexibility doesn't match for \(name)")
|
||||
}
|
||||
|
||||
func testForFullyFlexibleViews() {
|
||||
let layout = UICollectionViewLayout()
|
||||
runTestForFullyFlexibleView(UIView(), name: "UIView")
|
||||
runTestForFullyFlexibleView(UIScrollView(), name: "UIScrollView")
|
||||
runTestForFullyFlexibleView(UICollectionView(frame: .zero, collectionViewLayout: layout), name: "UICollectionView")
|
||||
runTestForFullyFlexibleView(UITableView(), name: "UITableView")
|
||||
runTestForFullyFlexibleView(UITextView(), name: "UITextView")
|
||||
}
|
||||
|
||||
func testForFixedSizeViews() {
|
||||
if let image = UIImage(systemName: "paperplane.fill") {
|
||||
runTestForFixedSizeViews(UIImageView(image: image), exectedSize: image.size, name: "UIImageView")
|
||||
} else {
|
||||
XCTFail("Could not find image with name 'paperplane.fill'")
|
||||
}
|
||||
|
||||
let largeConfig = UIImage.SymbolConfiguration(pointSize: 140, weight: .bold, scale: .large)
|
||||
if let image = UIImage(systemName: "doc.circle.fill", withConfiguration: largeConfig) {
|
||||
runTestForFixedSizeViews(UIImageView(image: image), exectedSize: image.size, name: "UIImageView")
|
||||
} else {
|
||||
XCTFail("Could not find image with name 'doc.circle.fill'")
|
||||
}
|
||||
runTestForFixedSizeViews(UISwitch(), exectedSize: UISwitch().sizeThatFits(CGSize(width: 100, height: 100)), name: "UISwitch")
|
||||
runTestForFixedSizeViews(UIStepper(), exectedSize: UIStepper().sizeThatFits(CGSize(width: 100, height: 100)), name: "UIStepper")
|
||||
runTestForFixedSizeViews(UIPageControl(), exectedSize: UIPageControl().sizeThatFits(CGSize(width: 100, height: 100)), name: "UIPageControl")
|
||||
runTestForFixedSizeViews(UIActivityIndicatorView(), exectedSize: UIActivityIndicatorView().sizeThatFits(CGSize(width: 100, height: 100)), name: "UIActivityIndicatorView")
|
||||
}
|
||||
|
||||
func testForHorizontallyExpandableViews() {
|
||||
let maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: .greatestFiniteMagnitude)
|
||||
runTestForHorizontallyExpandableView(UISlider(), height: UISlider().sizeThatFits(maxSize).height, name: "UISlider")
|
||||
runTestForHorizontallyExpandableView(UITextField(), height: UITextField().sizeThatFits(maxSize).height, name: "UITextField")
|
||||
runTestForHorizontallyExpandableView(UISearchBar(), height: UISearchBar().sizeThatFits(maxSize).height, name: "UISearchBar")
|
||||
runTestForHorizontallyExpandableView(UIProgressView(), height: UIProgressView().sizeThatFits(maxSize).height, name: "UIProgressView")
|
||||
runTestForHorizontallyExpandableView(UISearchTextField(), height: UISearchTextField().sizeThatFits(maxSize).height, name: "UISearchTextField")
|
||||
}
|
||||
|
||||
func testLabel() {
|
||||
let label1 = UILabel()
|
||||
label1.text = "Hello World"
|
||||
runTestForLabel(label1, name: "UILabel")
|
||||
|
||||
let label2 = CustomLabel()
|
||||
label2.text = "Hello World"
|
||||
runTestForLabel(label2, name: "Custom Label")
|
||||
|
||||
let label3 = CustomLabelWithOverrides()
|
||||
label3.text = "Hello World"
|
||||
runTestForLabel(label3, name: "Custom Label")
|
||||
XCTAssertEqual(8, label3.sizeThatFitsCallCounter, "sizeThatFitsCallCounter doesn't match for \(name)")
|
||||
}
|
||||
|
||||
func testButton() {
|
||||
let button1 = UIButton()
|
||||
button1.setTitle("Hello World", for: .normal)
|
||||
runTestForButton(button1, name: "UIButton")
|
||||
|
||||
let button2 = CustomButton()
|
||||
button2.setTitle("Hello World", for: .normal)
|
||||
runTestForButton(button2, name: "Custom Button")
|
||||
|
||||
let button3 = CustomButtonWithOverrides()
|
||||
button3.setTitle("Hello World", for: .normal)
|
||||
XCTAssertEqual(0, button3.sizeThatFitsCallCounter, "CustomButtonWithOverrides sizeThatFitsCallCounter doesn't match")
|
||||
runTestForButton(button3, name: "Custom Button")
|
||||
XCTAssertEqual(6, button3.sizeThatFitsCallCounter, "CustomButtonWithOverrides sizeThatFitsCallCounter doesn't match")
|
||||
}
|
||||
|
||||
func testForCustomViewsWithoutSizeThatFits() {
|
||||
let layout = UICollectionViewLayout()
|
||||
|
||||
runTestForFullyFlexibleView(CustomView(), name: "CustomView")
|
||||
runTestForFullyFlexibleView(CustomScrollView(), name: "CustomScrollView")
|
||||
runTestForFullyFlexibleView(CustomTableView(), name: "CustomTableView")
|
||||
runTestForFullyFlexibleView(CustomCollectionView(frame: .zero, collectionViewLayout: layout), name: "CustomCollectionView")
|
||||
runTestForFullyFlexibleView(CustomTextView(), name: "CustomTextView")
|
||||
|
||||
let largeConfig = UIImage.SymbolConfiguration(pointSize: 140, weight: .bold, scale: .large)
|
||||
if let image = UIImage(systemName: "doc.circle.fill", withConfiguration: largeConfig) {
|
||||
runTestForFixedSizeViews(CustomImageView(image: image), exectedSize: image.size, name: "UIImageView")
|
||||
} else {
|
||||
XCTFail("Could not find image with name 'doc.circle.fill'")
|
||||
}
|
||||
|
||||
let maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: .greatestFiniteMagnitude)
|
||||
runTestForHorizontallyExpandableView(CustomTextField(), height: CustomTextField().sizeThatFits(maxSize).height, name: "CustomTextField")
|
||||
runTestForHorizontallyExpandableView(CustomSearchBar(), height: CustomSearchBar().sizeThatFits(maxSize).height, name: "CustomSearchBar")
|
||||
runTestForHorizontallyExpandableView(CustomSlider(), height: CustomSlider().sizeThatFits(maxSize).height, name: "CustomSlider")
|
||||
runTestForHorizontallyExpandableView(CustomProgressView(), height: CustomProgressView().sizeThatFits(maxSize).height, name: "CustomProgressView")
|
||||
|
||||
runTestForFixedSizeViews(CustomSwitch(), exectedSize: CustomSwitch().sizeThatFits(CGSize(width: 100, height: 100)), name: "CustomSwitch")
|
||||
runTestForFixedSizeViews(CustomStepper(), exectedSize: CustomStepper().sizeThatFits(CGSize(width: 100, height: 100)), name: "CustomStepper")
|
||||
runTestForFixedSizeViews(CustomPageControl(), exectedSize: CustomPageControl().sizeThatFits(CGSize(width: 100, height: 100)), name: "CustomPageControl")
|
||||
runTestForFixedSizeViews(CustomActivityIndicatorView(), exectedSize: CustomActivityIndicatorView().sizeThatFits(CGSize(width: 100, height: 100)), name: "CustomActivityIndicatorView")
|
||||
}
|
||||
|
||||
func testCustomFullyFlexblyViewsWithMethodOverrides() {
|
||||
let layout = UICollectionViewLayout()
|
||||
let view = CustomViewWithOverrides()
|
||||
let scrollView = CustomScrollViewWithOverrides()
|
||||
let tableView = CustomTableViewWithOverrides()
|
||||
let collectionView = CustomCollectionViewWithOverrides(frame: .zero, collectionViewLayout: layout)
|
||||
let textView = CustomTextViewWithOverrides()
|
||||
|
||||
let viewCounter = view.sizeThatFitsCallCounter
|
||||
let scrollViewCounter = scrollView.sizeThatFitsCallCounter
|
||||
let tableViewCounter = tableView.sizeThatFitsCallCounter
|
||||
let collectionViewCounter = collectionView.sizeThatFitsCallCounter
|
||||
let textViewCounter = textView.sizeThatFitsCallCounter
|
||||
|
||||
_ = view.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = scrollView.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = tableView.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = collectionView.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = textView.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
|
||||
XCTAssertEqual(viewCounter + 1, view.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(scrollViewCounter + 1, scrollView.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(tableViewCounter + 1, tableView.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(collectionViewCounter + 1, collectionView.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(textViewCounter + 1, textView.sizeThatFitsCallCounter)
|
||||
|
||||
XCTAssertEqual(Flexibility.partial, view.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.partial, view.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, scrollView.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, scrollView.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, tableView.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, tableView.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, collectionView.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, collectionView.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, textView.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, textView.quick_flexibility(for: .vertical))
|
||||
}
|
||||
|
||||
func testCustomFixedViewsWithMethodOverrides() {
|
||||
let activityView = CustomActivityIndicatorWithOverrides()
|
||||
let stepper = CustomStepperWithOverrides()
|
||||
let switchView = CustomSwitchWithOverrides()
|
||||
let pageControl = CustomPageControlWithOverrides()
|
||||
|
||||
let activityViewCounter = activityView.sizeThatFitsCallCounter
|
||||
let stepperCounter = stepper.sizeThatFitsCallCounter
|
||||
let switchViewCounter = switchView.sizeThatFitsCallCounter
|
||||
let pageControlCounter = pageControl.sizeThatFitsCallCounter
|
||||
|
||||
_ = activityView.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = stepper.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = switchView.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = pageControl.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
|
||||
XCTAssertEqual(activityViewCounter + 1, activityView.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(stepperCounter + 1, stepper.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(switchViewCounter + 1, switchView.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(pageControlCounter + 1, pageControl.sizeThatFitsCallCounter)
|
||||
|
||||
XCTAssertEqual(Flexibility.fixedSize, activityView.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fixedSize, activityView.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fixedSize, stepper.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fixedSize, stepper.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fixedSize, switchView.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fixedSize, switchView.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fixedSize, pageControl.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fixedSize, pageControl.quick_flexibility(for: .vertical))
|
||||
}
|
||||
|
||||
func testCustomHorizontallyExpandebleViewsWithOverrides() {
|
||||
let searchBar = CustomSearchBarWithOverrides()
|
||||
let searchTextField = CustomSearchTextFieldWithOverrides()
|
||||
let slider = CustomSliderWithOverrides()
|
||||
let progressView = CustomProgressViewWithOverrides()
|
||||
let textField = CustomTextFieldWithOverrides()
|
||||
|
||||
let searchBarCounter = searchBar.sizeThatFitsCallCounter
|
||||
let searchTextFieldCounter = searchTextField.sizeThatFitsCallCounter
|
||||
let sliderCounter = slider.sizeThatFitsCallCounter
|
||||
let progressViewCounter = progressView.sizeThatFitsCallCounter
|
||||
let textFieldCounter = textField.sizeThatFitsCallCounter
|
||||
|
||||
_ = searchBar.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = searchTextField.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = slider.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = progressView.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
_ = textField.quick_layoutThatFits(CGSize(width: 10, height: 10))
|
||||
|
||||
XCTAssertEqual(searchBarCounter + 1, searchBar.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(searchTextFieldCounter + 1, searchTextField.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(sliderCounter + 1, slider.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(progressViewCounter + 1, progressView.sizeThatFitsCallCounter)
|
||||
XCTAssertEqual(textFieldCounter + 1, textField.sizeThatFitsCallCounter)
|
||||
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, searchBar.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fixedSize, searchBar.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, searchTextField.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fixedSize, searchTextField.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, slider.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fixedSize, slider.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, progressView.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fixedSize, progressView.quick_flexibility(for: .vertical))
|
||||
XCTAssertEqual(Flexibility.fullyFlexible, textField.quick_flexibility(for: .horizontal))
|
||||
XCTAssertEqual(Flexibility.fixedSize, textField.quick_flexibility(for: .vertical))
|
||||
}
|
||||
}
|
||||
|
||||
/// Testing custom views, that don't have a sizeThatFits overrides.
|
||||
private class CustomLabel: UILabel {}
|
||||
private class CustomButton: UIButton {}
|
||||
private class CustomView: UIView {}
|
||||
private class CustomScrollView: UIScrollView {}
|
||||
private class CustomCollectionView: UICollectionView {}
|
||||
private class CustomTableView: UITableView {}
|
||||
private class CustomTextView: UITextView {}
|
||||
private class CustomTextField: UITextField {}
|
||||
private class CustomSearchBar: UISearchBar {}
|
||||
private class CustomSlider: UISlider {}
|
||||
private class CustomProgressView: UIProgressView {}
|
||||
private class CustomSearchTextField: UISearchTextField {}
|
||||
private class CustomImageView: UIImageView {}
|
||||
private class CustomSwitch: UISwitch {}
|
||||
private class CustomStepper: UIStepper {}
|
||||
private class CustomPageControl: UIPageControl {}
|
||||
private class CustomActivityIndicatorView: UIActivityIndicatorView {}
|
||||
|
||||
private class CustomLabelWithOverrides: UILabel {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomButtonWithOverrides: UIButton {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomViewWithOverrides: UIView {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomScrollViewWithOverrides: UIScrollView {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomTableViewWithOverrides: UITableView {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomTextViewWithOverrides: UITextView {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomCollectionViewWithOverrides: UICollectionView {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomStepperWithOverrides: UIStepper {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomSwitchWithOverrides: UISwitch {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomPageControlWithOverrides: UIPageControl {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomActivityIndicatorWithOverrides: UIActivityIndicatorView {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomTextFieldWithOverrides: UITextField {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomSearchBarWithOverrides: UISearchBar {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomSliderWithOverrides: UISlider {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomProgressViewWithOverrides: UIProgressView {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomSearchTextFieldWithOverrides: UISearchTextField {
|
||||
var sizeThatFitsCallCounter = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCallCounter += 1
|
||||
return super.sizeThatFits(size)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutBridge
|
||||
import QuickLayoutCore
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class ViewExtractionTests: XCTestCase {
|
||||
func testViewExtraction() {
|
||||
|
||||
let view1 = UIView()
|
||||
let view2 = UIView()
|
||||
let view3 = UIView()
|
||||
let view4 = UIView()
|
||||
let view5 = UIView()
|
||||
let view6 = UIView()
|
||||
let view7 = UIView()
|
||||
let view8 = UIView()
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
view2
|
||||
VStack {
|
||||
view3
|
||||
view4
|
||||
}
|
||||
ZStack {
|
||||
view5
|
||||
view6
|
||||
}
|
||||
view7
|
||||
.overlay {
|
||||
view8
|
||||
}
|
||||
}
|
||||
let result = layout.views()
|
||||
let expectedViews = [view1, view2, view3, view4, view5, view6, view7, view8]
|
||||
|
||||
XCTAssertEqual(result, expectedViews)
|
||||
}
|
||||
|
||||
func testViewExtractionFromHstackWithConditionalStatements() {
|
||||
|
||||
let view1 = UIView()
|
||||
let view2 = UIView()
|
||||
let view3 = UIView()
|
||||
let view4 = UIView()
|
||||
let view5 = UIView()
|
||||
let view6 = UIView()
|
||||
let view7 = UIView()
|
||||
|
||||
let aBoolValue = true
|
||||
|
||||
let anArray1 = [view4, view5]
|
||||
let anArray2 = [view6, view7]
|
||||
|
||||
let layout = HStack {
|
||||
view1
|
||||
if !aBoolValue {
|
||||
view2
|
||||
}
|
||||
if aBoolValue {
|
||||
view3
|
||||
}
|
||||
for view in anArray1 {
|
||||
view
|
||||
}
|
||||
ForEach(anArray2)
|
||||
}
|
||||
let result = layout.views()
|
||||
let expectedViews = [view1, view3, view4, view5, view6, view7]
|
||||
|
||||
XCTAssertEqual(result, expectedViews)
|
||||
}
|
||||
|
||||
func testViewExtractionFromZstackWithConditionalStatements() {
|
||||
|
||||
let view1 = UIView()
|
||||
let view2 = UIView()
|
||||
let view3 = UIView()
|
||||
let view4 = UIView()
|
||||
let view5 = UIView()
|
||||
let view6 = UIView()
|
||||
let view7 = UIView()
|
||||
|
||||
let aBoolValue = true
|
||||
|
||||
let anArray1 = [view4, view5]
|
||||
let anArray2 = [view6, view7]
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
if !aBoolValue {
|
||||
view2
|
||||
}
|
||||
if aBoolValue {
|
||||
view3
|
||||
}
|
||||
for view in anArray1 {
|
||||
view
|
||||
}
|
||||
ForEach(anArray2)
|
||||
}
|
||||
let result = layout.views()
|
||||
let expectedViews = [view1, view3, view4, view5, view6, view7]
|
||||
|
||||
XCTAssertEqual(result, expectedViews)
|
||||
}
|
||||
|
||||
func testViewExtractionFormLayoutPrimitives() {
|
||||
|
||||
let view1 = UIView()
|
||||
let view2 = UIView()
|
||||
let view3 = UIView()
|
||||
let view4 = UIView()
|
||||
let view5 = UIView()
|
||||
let view6 = UIView()
|
||||
let view7 = UIView()
|
||||
let view8 = UIView()
|
||||
|
||||
let layout = ZStack {
|
||||
view1
|
||||
EmptyLayout()
|
||||
view2.frame(width: 100, height: 100)
|
||||
view3.resizable().frame(width: 100, height: 100)
|
||||
view4.expand(by: CGSize(width: 1, height: 1))
|
||||
Spacer()
|
||||
view5.frame(minWidth: 100)
|
||||
view6.aspectRatio(CGSize(width: 1, height: 1))
|
||||
view7.layoutPriority(1)
|
||||
view8.padding(10)
|
||||
}
|
||||
let result = layout.views()
|
||||
let expectedViews = [view1, view2, view3, view4, view5, view6, view7, view8]
|
||||
|
||||
XCTAssertEqual(result, expectedViews)
|
||||
}
|
||||
|
||||
func testBackgroundAndOverlay() {
|
||||
|
||||
let view1 = UIView()
|
||||
let view2 = UIView()
|
||||
let view3 = UIView()
|
||||
let view4 = UIView()
|
||||
|
||||
let layout = ZStack {
|
||||
LayeringElement(
|
||||
target: view1,
|
||||
layer: view2,
|
||||
type: .overlay,
|
||||
alignment: .center
|
||||
)
|
||||
LayeringElement(
|
||||
target: view3,
|
||||
layer: view4,
|
||||
type: .background,
|
||||
alignment: .center
|
||||
)
|
||||
}
|
||||
let result = layout.views()
|
||||
let expectedViews = [view1, view2, view4, view3]
|
||||
|
||||
XCTAssertEqual(result, expectedViews)
|
||||
}
|
||||
|
||||
func testBackgroundAndOverlayInversed() {
|
||||
|
||||
let view1 = UIView()
|
||||
let view2 = UIView()
|
||||
let view3 = UIView()
|
||||
let view4 = UIView()
|
||||
|
||||
let layout = ZStack {
|
||||
LayeringElement(
|
||||
target: view3,
|
||||
layer: view4,
|
||||
type: .background,
|
||||
alignment: .center
|
||||
)
|
||||
LayeringElement(
|
||||
target: view1,
|
||||
layer: view2,
|
||||
type: .background,
|
||||
alignment: .center
|
||||
)
|
||||
}
|
||||
let result = layout.views()
|
||||
let expectedViews = [view4, view3, view2, view1]
|
||||
|
||||
XCTAssertEqual(result, expectedViews)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutBridge
|
||||
|
||||
@QuickLayout
|
||||
final class BodyCreationTestView: UIView {
|
||||
|
||||
var bodyCounter = 0
|
||||
|
||||
@Invalidating(.layout)
|
||||
var frameSize = CGSize.zero
|
||||
|
||||
var body: any Layout {
|
||||
countedLayout()
|
||||
}
|
||||
|
||||
func countedLayout() -> any Layout {
|
||||
bodyCounter += 1
|
||||
return VStack {
|
||||
EmptyLayout()
|
||||
.frame(width: frameSize.width, height: frameSize.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import QuickLayoutBridge
|
||||
|
||||
final class LeafView: UIView {
|
||||
var sizeThatFitsCounter = 0
|
||||
|
||||
@Invalidating(.layout)
|
||||
var frameSize = CGSize.zero
|
||||
|
||||
@Invalidating(.layout)
|
||||
var property: Int = 0
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
sizeThatFitsCounter += 1
|
||||
return frameSize
|
||||
}
|
||||
}
|
||||
|
||||
@QuickLayout
|
||||
final class PrivateChildView: UIView {
|
||||
var bodyCounter: Int = 0
|
||||
|
||||
@Invalidating(.layout)
|
||||
var property: Int = 0
|
||||
|
||||
let leafView = LeafView()
|
||||
|
||||
var body: any Layout {
|
||||
countedLayout()
|
||||
}
|
||||
|
||||
private func countedLayout() -> any Layout {
|
||||
bodyCounter += 1
|
||||
return VStack {
|
||||
leafView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@QuickLayout
|
||||
final class BodyCreationWithNestedViewsTestView: UIView {
|
||||
|
||||
var bodyCounter = 0
|
||||
|
||||
let childView1 = PrivateChildView()
|
||||
let childView2 = PrivateChildView()
|
||||
|
||||
@Invalidating(.layout)
|
||||
var property: Int = 0
|
||||
|
||||
var body: any Layout {
|
||||
countedLayout()
|
||||
}
|
||||
|
||||
private func countedLayout() -> any Layout {
|
||||
bodyCounter += 1
|
||||
return HStack {
|
||||
VStack {
|
||||
childView1
|
||||
childView2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue