Compare commits

..

7 Commits

Author SHA1 Message Date
Shin Yamamoto 8f2be39bf4 Version 2.8.2 2024-02-18 15:25:08 +09:00
Shin Yamamoto 92c10830ff ci: use Xcode 15.2 on the GitHub Actions (#619)
- Replaced 'runsOn' with 'runs-on'.
- Supported build jobs for Xcode 15.2.
- Didn't use `macos-11` as possible which was deprecated.
2024-02-18 14:46:32 +09:00
Shin Yamamoto dcb89f58c3 Add CoreTest.test_handleGesture_endWithoutAttraction() 2024-02-17 09:04:09 +09:00
Shin Yamamoto d39c4b54d1 Enable to define and use a subclass object of BackdropView (#617)
* Enable to create a subclass of BackdropView
* Add a custom backdrop sample in the Samples example
2024-02-16 22:07:14 +09:00
Ortwin Gentz, FutureTap 504182ceae Fix scroll locking behavior (#615)
use a separate `scrollLocked` var instead of abusing the scrollView's properties to store the locked state;
fixes an issue where the scroll indicators were no longer visible because lockScrollView() was executed twice before unlockScrollView() was called (due to the user changing `showsVerticalScrollIndicator` mid-animation);
2024-02-16 22:04:10 +09:00
Shin Yamamoto bc1cfe444b Fix a bug state was not changed property after v2.8.1
The state was not changed after moving a panel without attractive
interaction. For example, a panel is moved from half to full and
the scroll content continues to scroll with its deceleration
animation. We can test it on 'Show tracking(TextView)' in Samples app.

dbef6a6 commit causes this issue.
2024-02-03 13:36:36 +09:00
Shin Yamamoto 0e0f773df7 Possible fix for #586 2024-02-03 10:27:21 +09:00
16 changed files with 202 additions and 105 deletions
+75 -27
View File
@@ -11,7 +11,7 @@ on:
jobs:
build:
runs-on: ${{ matrix.runsOn }}
runs-on: ${{ matrix.runs-on }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
@@ -19,67 +19,72 @@ jobs:
matrix:
include:
- swift: "5.9"
xcode: "15.0.1"
runsOn: macos-13
xcode: "15.2"
runs-on: macos-13
- swift: "5.8"
xcode: "14.3.1"
runsOn: macos-13
runs-on: macos-13
- swift: "5.7"
xcode: "14.1"
runsOn: macos-12
runs-on: macos-12
- swift: "5.6"
xcode: "13.4.1"
runsOn: macos-12
runs-on: macos-12
- swift: "5.5"
xcode: "13.2.1"
runsOn: macos-11
runs-on: macos-12
- swift: "5.4"
xcode: "12.5.1"
runsOn: macos-11
runs-on: macos-11
- swift: "5.3"
xcode: "12.4"
runsOn: macos-11
runs-on: macos-11
- swift: "5.2"
xcode: "11.7"
runsOn: macos-11
runs-on: macos-11
steps:
- uses: actions/checkout@v3
- name: Building in Swift ${{ matrix.swift }}
run: xcodebuild -scheme FloatingPanel SWIFT_VERSION=${{ matrix.swift }} clean build
test:
runs-on: ${{ matrix.runsOn }}
runs-on: ${{ matrix.runs-on }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
- os: "17.0.1"
xcode: "15.0.1"
- os: "17.2"
xcode: "15.2"
sim: "iPhone 15 Pro"
parallel: NO # Stop random test job failures
runsOn: macos-13
runs-on: macos-13
- os: "16.4"
xcode: "14.3.1"
sim: "iPhone 14 Pro"
parallel: NO # Stop random test job failures
runsOn: macos-13
runs-on: macos-13
- os: "15.5"
xcode: "13.4.1"
sim: "iPhone 13 Pro"
parallel: NO # Stop random test job failures
runsOn: macos-12
runs-on: macos-12
steps:
- uses: actions/checkout@v3
- name: Testing in iOS ${{ matrix.os }}
run: xcodebuild clean test -scheme FloatingPanel -workspace FloatingPanel.xcworkspace -destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}' -parallel-testing-enabled '${{ matrix.parallel }}'
run: |
xcodebuild clean test \
-workspace FloatingPanel.xcworkspace \
-scheme FloatingPanel \
-destination 'platform=iOS Simulator,OS=${{ matrix.os }},name=${{ matrix.sim }}' \
-parallel-testing-enabled '${{ matrix.parallel }}'
timeout-minutes: 20
example:
runs-on: macos-12
runs-on: macos-13
env:
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
strategy:
fail-fast: false
matrix:
@@ -91,26 +96,67 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Building ${{ matrix.example }}
run: xcodebuild -workspace FloatingPanel.xcworkspace -scheme ${{ matrix.example }} -sdk iphonesimulator clean build
run: |
xcodebuild clean build \
-workspace FloatingPanel.xcworkspace \
-scheme ${{ matrix.example }} \
-sdk iphonesimulator
swiftpm:
runs-on: macos-12
runs-on: macos-13
env:
DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
strategy:
fail-fast: false
matrix:
platform: [iphoneos, iphonesimulator]
arch: [x86_64, arm64]
exclude:
- platform: iphoneos
arch: x86_64
include:
# 17.2
- platform: iphoneos
sys: "ios17.2"
- platform: iphonesimulator
sys: "ios17.2-simulator"
steps:
- uses: actions/checkout@v3
- name: "Swift Package Manager build"
run: |
xcrun swift build \
--sdk "$(xcrun --sdk ${{ matrix.platform }} --show-sdk-path)" \
-Xswiftc "-target" -Xswiftc "${{ matrix.arch }}-apple-${{ matrix.sys }}"
swiftpm_old:
runs-on: ${{ matrix.runs-on }}
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
strategy:
fail-fast: false
matrix:
include:
# 16.4
- target: "x86_64-apple-ios16.4-simulator"
xcode: "14.3.1"
runs-on: macos-13
- target: "arm64-apple-ios16.4-simulator"
xcode: "14.3.1"
runs-on: macos-13
# 15.7
- target: "x86_64-apple-ios15.7-simulator"
xcode: "14.1"
runs-on: macos-12
- target: "arm64-apple-ios15.7-simulator"
# 16.1
- target: "x86_64-apple-ios16.1-simulator"
- target: "arm64-apple-ios16.1-simulator"
xcode: "14.1"
runs-on: macos-12
steps:
- uses: actions/checkout@v3
- name: "Swift Package Manager build"
run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
run: |
swift build \
-Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" \
-Xswiftc "-target" -Xswiftc "${{ matrix.target }}"
carthage:
runs-on: macos-11
@@ -120,7 +166,9 @@ jobs:
run: carthage build --use-xcframeworks --no-skip-current
cocoapods:
runs-on: macos-12
runs-on: macos-13
env:
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
steps:
- uses: actions/checkout@v3
- name: "CocoaPods: pod lib lint"
@@ -82,10 +82,7 @@ struct FloatingPanelView<Content: View, FloatingPanelContent: View>: UIViewContr
/// Responsible to setup the view hierarchy and floating panel.
final class Coordinator {
private let parent: FloatingPanelView<Content, FloatingPanelContent>
private lazy var fpc = {
FloatingPanelController.enableDismissToRemove()
return FloatingPanelController()
}()
private lazy var fpc = FloatingPanelController()
init(parent: FloatingPanelView<Content, FloatingPanelContent>) {
self.parent = parent
+1 -6
View File
@@ -1,14 +1,9 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FloatingPanelController.enableDismissToRemove()
return true
}
}
+1 -1
View File
@@ -124,7 +124,7 @@ extension MainViewController: UISearchBarDelegate {
searchBar.showsCancelButton = true
searchVC.showHeader(animated: true)
searchVC.tableView.alpha = 1.0
detailFpc.removePanelFromParent(animated: true)
detailVC.dismiss(animated: true, completion: nil)
}
func deactivate(searchBar: UISearchBar) {
searchBar.resignFirstResponder()
@@ -1,13 +1,8 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
import UIKit
import FloatingPanel
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FloatingPanelController.enableDismissToRemove()
return true
}
}
@@ -24,6 +24,7 @@ enum UseCase: Int, CaseIterable {
case showAdaptivePanel
case showAdaptivePanelWithCustomGuide
case showCustomStatePanel
case showCustomBackdrop
}
extension UseCase {
@@ -50,6 +51,7 @@ extension UseCase {
case .showAdaptivePanel: return "Show Adaptive Panel"
case .showAdaptivePanelWithCustomGuide: return "Show Adaptive Panel (Custom Layout Guide)"
case .showCustomStatePanel: return "Show Panel with Custom state"
case .showCustomBackdrop: return "Show Panel with Custom Backdrop"
}
}
}
@@ -83,6 +85,7 @@ extension UseCase {
case .showAdaptivePanel: return .storyboard(String(describing: ImageViewController.self))
case .showAdaptivePanelWithCustomGuide: return .storyboard(String(describing: AdaptiveLayoutTestViewController.self))
case .showCustomStatePanel: return .viewController(DebugTableViewController())
case .showCustomBackdrop: return .viewController(UIViewController())
}
}
@@ -273,6 +273,52 @@ extension UseCaseController {
fpc.set(contentViewController: contentVC)
fpc.ext_trackScrollView(in: contentVC)
addMain(panel: fpc)
case .showCustomBackdrop:
class BlurBackdropView: BackdropView {
var effectView: UIVisualEffectView!
override var alpha: CGFloat {
set {
effectView.alpha = newValue
}
get {
effectView.alpha
}
}
override init() {
super.init()
let effect = UIBlurEffect(style: .prominent)
let effectView = UIVisualEffectView(effect: effect)
addSubview(effectView)
effectView.frame = bounds
effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.effectView = effectView
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class CustomBottomLayout: FloatingPanelBottomLayout {
override var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
return [
.full: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(fractionalInset: 0.1, edge: .bottom, referenceGuide: .safeArea),
]
}
override func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
return state == .full ? 0.8 : 0.0
}
}
let fpc = FloatingPanelController()
fpc.delegate = self
fpc.set(contentViewController: contentVC)
fpc.backdropView = BlurBackdropView()
fpc.layout = CustomBottomLayout()
addMain(panel: fpc)
}
}
@@ -1,15 +1,9 @@
// Copyright 2018 the FloatingPanel authors. All rights reserved. MIT license.
#import "AppDelegate.h"
@import FloatingPanel;
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions
{
[FloatingPanelController enableDismissToRemove];
return YES;
}
@end
+1 -1
View File
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = "FloatingPanel"
s.version = "2.8.1"
s.version = "2.8.2"
s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface."
s.description = <<-DESC
FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app.
+1 -1
View File
@@ -9,7 +9,7 @@
FloatingPanel is a simple and easy-to-use UI component designed for a user interface featured in Apple Maps, Shortcuts and Stocks app.
The user interface displays related content and utilities alongside the main content.
Please see also [the API reference](https://floatingpanel.github.io/2.8.1/documentation/floatingpanel/) for more details.
Please see also [the API reference@SPI](https://swiftpackageindex.com/scenee/FloatingPanel/2.8.2/documentation/floatingpanel) for more details.
![Maps](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps.gif)
![Stocks](https://github.com/SCENEE/FloatingPanel/blob/master/assets/stocks.gif)
+3 -3
View File
@@ -4,7 +4,7 @@ import UIKit
/// A view that presents a backdrop interface behind a panel.
@objc(FloatingPanelBackdropView)
public class BackdropView: UIView {
open class BackdropView: UIView {
/// The gesture recognizer for tap gestures to dismiss a panel.
///
@@ -12,14 +12,14 @@ public class BackdropView: UIView {
/// To dismiss a panel by tap gestures on the backdrop, `dismissalTapGestureRecognizer.isEnabled` is set to true.
@objc public var dismissalTapGestureRecognizer: UITapGestureRecognizer
init() {
public init() {
dismissalTapGestureRecognizer = UITapGestureRecognizer()
dismissalTapGestureRecognizer.isEnabled = false
super.init(frame: .zero)
addGestureRecognizer(dismissalTapGestureRecognizer)
}
required init?(coder: NSCoder) {
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
+14 -24
View File
@@ -158,14 +158,15 @@ open class FloatingPanelController: UIViewController {
/// Returns the surface view managed by the controller object. It's the same as `self.view`.
@objc
public var surfaceView: SurfaceView! {
public var surfaceView: SurfaceView {
return floatingPanel.surfaceView
}
/// Returns the backdrop view managed by the controller object.
@objc
public var backdropView: BackdropView! {
return floatingPanel.backdropView
public var backdropView: BackdropView {
set { floatingPanel.backdropView = newValue }
get { return floatingPanel.backdropView }
}
/// Returns the scroll view that the controller tracks.
@@ -288,6 +289,8 @@ open class FloatingPanelController: UIViewController {
}
private func setUp() {
_ = FloatingPanelController.dismissSwizzling
modalPresentationStyle = .custom
transitioningDelegate = modalTransition
@@ -308,7 +311,7 @@ open class FloatingPanelController: UIViewController {
}
}
// MARK: - Overrides
// MARK:- Overrides
/// Creates the view that the controller manages.
open override func loadView() {
@@ -379,8 +382,7 @@ open class FloatingPanelController: UIViewController {
safeAreaInsetsObservation = nil
}
// MARK: - Child view controller to consult
// MARK:- Child view controller to consult
open override var childForStatusBarStyle: UIViewController? {
return contentViewController
}
@@ -397,19 +399,19 @@ open class FloatingPanelController: UIViewController {
return contentViewController
}
// MARK: - Privates
// MARK:- Privates
private func shouldUpdateLayout(from previous: UITraitCollection, to new: UITraitCollection) -> Bool {
return previous.horizontalSizeClass != new.horizontalSizeClass
|| previous.verticalSizeClass != new.verticalSizeClass
|| previous.preferredContentSizeCategory != new.preferredContentSizeCategory
|| previous.layoutDirection != new.layoutDirection
|| previous.verticalSizeClass != new.verticalSizeClass
|| previous.preferredContentSizeCategory != new.preferredContentSizeCategory
|| previous.layoutDirection != new.layoutDirection
}
private func update(safeAreaInsets: UIEdgeInsets) {
guard
preSafeAreaInsets != safeAreaInsets
else { return }
else { return }
os_log(msg, log: devLog, type: .debug, "Update safeAreaInsets = \(safeAreaInsets)")
@@ -539,7 +541,7 @@ open class FloatingPanelController: UIViewController {
self.view.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0),
self.view.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0),
self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0),
])
])
show(animated: animated) { [weak self] in
guard let self = self else { return }
@@ -693,18 +695,6 @@ open class FloatingPanelController: UIViewController {
get { floatingPanel.layoutAdapter.surfaceLocation }
set { floatingPanel.layoutAdapter.surfaceLocation = newValue }
}
/// Calling this will allow to invoke `removePanelFromParent(animated:completion:)` as needed by
/// calling UIViewController's `dismiss` method
///
/// Previously, until v2.8, this was the default behavior. However, from v2.9 onwards, due to
/// identified issues when used in conjunction with other libraries, it has been made an opt-in
/// feature.
@objc
public static func enableDismissToRemove() {
_ = FloatingPanelController.dismissSwizzling
}
}
extension FloatingPanelController {
+34 -20
View File
@@ -10,7 +10,13 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private weak var ownerVC: FloatingPanelController?
let surfaceView: SurfaceView
let backdropView: BackdropView
var backdropView: BackdropView {
didSet {
backdropView.dismissalTapGestureRecognizer
.addTarget(self, action: #selector(handleBackdrop(tapGesture:)))
}
}
let layoutAdapter: LayoutAdapter
let behaviorAdapter: BehaviorAdapter
@@ -24,6 +30,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
scrollBounce = cur.bounces
scrollIndictorVisible = cur.showsVerticalScrollIndicator
}
scrollLocked = false
} else {
if let pre = oldValue {
pre.isDirectionalLockEnabled = false
@@ -68,6 +75,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private var scrollBounce = false
private var scrollIndictorVisible = false
private var scrollBounceThreshold: CGFloat = -30.0
private var scrollLocked = false
// MARK: - Interface
@@ -728,13 +736,7 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
guard shouldAttract(to: target) else {
self.updateLayout(to: target)
self.unlockScrollView()
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
// This allows library users to get the correct state in the delegate method.
if let vc = ownerVC {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
}
self.endWithoutAttraction(target)
return
}
@@ -877,6 +879,17 @@ class Core: NSObject, UIGestureRecognizerDelegate {
return true
}
func endWithoutAttraction(_ target: FloatingPanelState) {
self.state = target
self.updateLayout(to: target)
self.unlockScrollView()
// The `floatingPanelDidEndDragging(_:willAttract:)` must be called after the state property changes.
// This allows library users to get the correct state in the delegate method.
if let vc = ownerVC {
vc.delegate?.floatingPanelDidEndDragging?(vc, willAttract: false)
}
}
private func startAttraction(to state: FloatingPanelState, with velocity: CGPoint, completion: @escaping (() -> Void)) {
os_log(msg, log: devLog, type: .debug, "startAnimation to \(state) -- velocity = \(value(of: velocity))")
guard let vc = ownerVC else { return }
@@ -1051,29 +1064,25 @@ class Core: NSObject, UIGestureRecognizerDelegate {
private func lockScrollView(strict: Bool = false) {
guard let scrollView = scrollView else { return }
if scrollLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll locked")
return
}
scrollBounce = scrollView.bounces
if !strict, shouldLooselyLockScrollView {
if scrollView.isLooselyLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll locked loosely.")
return
}
// Don't change its `bounces` property. If it's changed, it will cause its scroll content offset jump at
// the most expanded anchor position while seamlessly scrolling content. This problem only occurs where its
// content mode is `.fitToBounds` and the tracking scroll content is smaller than the content view size.
// The reason why is because `bounces` prop change leads to the "content frame" change on `.fitToBounds`.
// See also https://github.com/scenee/FloatingPanel/issues/524.
} else {
if scrollView.isLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll locked.")
return
}
scrollBounce = scrollView.bounces
scrollView.bounces = false
}
os_log(msg, log: devLog, type: .debug, "lock scroll view")
scrollView.isDirectionalLockEnabled = true
scrollLocked = true
scrollView.isDirectionalLockEnabled = true
switch layoutAdapter.position {
case .top, .bottom:
scrollIndictorVisible = scrollView.showsVerticalScrollIndicator
@@ -1085,9 +1094,14 @@ class Core: NSObject, UIGestureRecognizerDelegate {
}
private func unlockScrollView() {
guard let scrollView = scrollView, scrollView.isLocked else { return }
guard let scrollView = scrollView else { return }
if !scrollLocked {
os_log(msg, log: devLog, type: .debug, "Already scroll unlocked.")
return
}
os_log(msg, log: devLog, type: .debug, "unlock scroll view")
scrollLocked = false
scrollView.bounces = scrollBounce
scrollView.isDirectionalLockEnabled = false
switch layoutAdapter.position {
-6
View File
@@ -109,12 +109,6 @@ extension UIGestureRecognizer.State: CustomDebugStringConvertible {
#endif
extension UIScrollView {
var isLocked: Bool {
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
}
var isLooselyLocked: Bool {
return !showsVerticalScrollIndicator && isDirectionalLockEnabled
}
var fp_contentOffsetMax: CGPoint {
return CGPoint(x: max((contentSize.width + adjustedContentInset.right) - bounds.width, 0.0),
y: max((contentSize.height + adjustedContentInset.bottom) - bounds.height, 0.0))
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.8.1</string>
<string>2.8.2</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
+21
View File
@@ -913,6 +913,27 @@ class CoreTests: XCTestCase {
)
}
}
func test_handleGesture_endWithoutAttraction() throws {
class Delegate: FloatingPanelControllerDelegate {
var willAttract: Bool?
func floatingPanelDidEndDragging(_ fpc: FloatingPanelController, willAttract attract: Bool) {
willAttract = attract
}
}
let fpc = FloatingPanelController()
let scrollView = UIScrollView()
let delegate = Delegate()
fpc.showForTest()
fpc.delegate = delegate
XCTAssertEqual(fpc.state, .half)
fpc.floatingPanel.endWithoutAttraction(.full)
XCTAssertEqual(fpc.state, .full)
XCTAssertEqual(fpc.surfaceLocation(for: .full).y, fpc.surfaceLocation.y)
XCTAssertEqual(delegate.willAttract, false)
}
}
private class FloatingPanelLayout3Positions: FloatingPanelTestLayout {