Initial commit

fbshipit-source-id: ac9cb65f0bb8a29acff5b6664ede5c4ce7a1a632
This commit is contained in:
facebook-github-bot 2025-11-14 14:11:07 -08:00
commit cf25689181
275 changed files with 36505 additions and 0 deletions

69
.github/workflows/docs.yml vendored Normal file
View 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
View 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
View 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
View 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.

View 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
View 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
View 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
View 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 dont 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 stacks 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 &#x2022; <a href="https://opensource.fb.com/legal/terms">Terms of Use</a> &#x2022; <a href="https://opensource.fb.com/legal/privacy">Privacy Policy</a>

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 Citys 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 Citys legendary Angels Share. After the beloved speakeasy closed, Watanabe opened Martinys, 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")!),
]
}()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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()
}
}

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

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

View file

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

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

View file

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

View file

@ -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] = []
}

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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