diff --git a/LayoutKit.xcodeproj/project.pbxproj b/LayoutKit.xcodeproj/project.pbxproj index ce9c71d8..a57cf8d3 100644 --- a/LayoutKit.xcodeproj/project.pbxproj +++ b/LayoutKit.xcodeproj/project.pbxproj @@ -298,6 +298,9 @@ CDD4F71320EC728200DB358C /* IndexSetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B193BB71D887BCF00FCA22D /* IndexSetExtension.swift */; }; CDD4F71420EC728300DB358C /* IndexSetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B193BB71D887BCF00FCA22D /* IndexSetExtension.swift */; }; CDD4F71520EC728300DB358C /* IndexSetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B193BB71D887BCF00FCA22D /* IndexSetExtension.swift */; }; + D338ADAA2109ADDA007A2006 /* RotatingStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D338ADA92109ADDA007A2006 /* RotatingStackTests.swift */; }; + D3DE670721071BD700E17692 /* AutoRotateStackLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DE670621071BD700E17692 /* AutoRotateStackLayout.swift */; }; + D3DE6709210738BE00E17692 /* AutoRotateStackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DE6708210738BE00E17692 /* AutoRotateStackViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -530,6 +533,9 @@ 7EECD0622053916C003DC4B1 /* LayoutKit-iOS copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "LayoutKit-iOS copy-Info.plist"; path = "/Users/staguer/ws/lk0/LayoutKit-iOS copy-Info.plist"; sourceTree = ""; }; AD2C36421EA5AF9500550A03 /* ReloadableViewLayoutAdapterCollectionViewOverrideTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReloadableViewLayoutAdapterCollectionViewOverrideTests.swift; sourceTree = ""; }; ADE5FCBF1EA5B5C8006A3DC2 /* ReloadableViewLayoutAdapterTableViewOverrideTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReloadableViewLayoutAdapterTableViewOverrideTests.swift; sourceTree = ""; }; + D338ADA92109ADDA007A2006 /* RotatingStackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotatingStackTests.swift; sourceTree = ""; }; + D3DE670621071BD700E17692 /* AutoRotateStackLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoRotateStackLayout.swift; sourceTree = ""; }; + D3DE6708210738BE00E17692 /* AutoRotateStackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoRotateStackViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -636,6 +642,7 @@ 0B2D09DB1D87365F007E487C /* StackViewController.swift */, 0B2D09DC1D87365F007E487C /* UrlImageLayout.swift */, 0B6B04361DC8402E00F23EEA /* DWURecyclingAlert.m */, + D3DE6708210738BE00E17692 /* AutoRotateStackViewController.swift */, ); path = LayoutKitSampleApp; sourceTree = ""; @@ -750,6 +757,7 @@ 44F968161E42639500392763 /* TextViewLayoutTests.swift */, 0BCB76671D8725310065E02A /* UIFontExtension.swift */, 0BCB76681D8725310065E02A /* ViewRecyclerTests.swift */, + D338ADA92109ADDA007A2006 /* RotatingStackTests.swift */, ); path = LayoutKitTests; sourceTree = ""; @@ -795,6 +803,7 @@ 0BCB75E41D8724800065E02A /* SizeLayout.swift */, 0BCB75E51D8724800065E02A /* StackLayout.swift */, 44F968141E425F5D00392763 /* TextViewLayout.swift */, + D3DE670621071BD700E17692 /* AutoRotateStackLayout.swift */, ); path = Layouts; sourceTree = ""; @@ -1314,6 +1323,7 @@ 0B2D09EE1D87365F007E487C /* Stopwatch.swift in Sources */, 0B2D09E31D87365F007E487C /* BatchUpdatesBaseViewController.swift in Sources */, 0B2D09F81D87365F007E487C /* StackViewController.swift in Sources */, + D3DE6709210738BE00E17692 /* AutoRotateStackViewController.swift in Sources */, 0B6B04371DC8402E00F23EEA /* DWURecyclingAlert.m in Sources */, 0B2D09F51D87365F007E487C /* LabledImageLayout.swift in Sources */, 0B2D09F11D87365F007E487C /* FeedCollectionViewController.swift in Sources */, @@ -1367,6 +1377,7 @@ 0B765F2C1DC0514F000BF1FD /* CGFloatExtension.swift in Sources */, 0BCB76041D8724800065E02A /* SizeLayout.swift in Sources */, 0BCB76031D8724800065E02A /* LabelLayout.swift in Sources */, + D3DE670721071BD700E17692 /* AutoRotateStackLayout.swift in Sources */, 0BCB76101D8724800065E02A /* ReloadableViewLayoutAdapter+UITableView.swift in Sources */, 0BCB75FB1D8724800065E02A /* Flexibility.swift in Sources */, 0BCB76121D8724800065E02A /* ReloadableViewUpdateManager.swift in Sources */, @@ -1406,6 +1417,7 @@ 0B2D09361D872F75007E487C /* TestStack.swift in Sources */, 0B2D09351D872F75007E487C /* TableViewTests.swift in Sources */, 0B2D09311D872F75007E487C /* StackLayoutFlexibilityTests.swift in Sources */, + D338ADAA2109ADDA007A2006 /* RotatingStackTests.swift in Sources */, 0B2D09301D872F75007E487C /* StackLayoutDistributionTests.swift in Sources */, 0B2D09331D872F75007E487C /* StackLayoutTests.swift in Sources */, 0B2D09291D872F75007E487C /* LayoutArrangementTests.swift in Sources */, diff --git a/LayoutKitSampleApp/AutoRotateStackViewController.swift b/LayoutKitSampleApp/AutoRotateStackViewController.swift new file mode 100644 index 00000000..e3c32524 --- /dev/null +++ b/LayoutKitSampleApp/AutoRotateStackViewController.swift @@ -0,0 +1,45 @@ +// Copyright 2018 LinkedIn Corp. +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import UIKit +import LayoutKit + +class AutoRotateStackViewController: UIViewController { + + private var autoRotateStackLayout: AutoRotateStackLayout? + + override func viewDidLoad() { + super.viewDidLoad() + edgesForExtendedLayout = UIRectEdge() + view.backgroundColor = UIColor.white + + let helloWorldLayout1 = ButtonLayout(type: .system, title: "Hello World! "); + let helloWorldLayout2 = ButtonLayout(type: .system, title: "Hello World! "); + let helloWorldLayout3 = ButtonLayout(type: .system, title: "Hello World! "); + let helloWorldLayout4 = ButtonLayout(type: .system, title: "Hello World! "); + let helloWorldLayout5 = ButtonLayout(type: .system, title: "Hello World! "); + + let autoRotateStackLayout = AutoRotateStackLayout(sublayouts: [ + helloWorldLayout1, + helloWorldLayout2, + helloWorldLayout3, + helloWorldLayout4, + helloWorldLayout5 + ]) + autoRotateStackLayout.arrangement(width: view.frame.width).makeViews(in: view) + self.autoRotateStackLayout = autoRotateStackLayout + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + if let autoRotateStackLayout = autoRotateStackLayout { + autoRotateStackLayout.arrangement(width: view.frame.width).makeViews(in: view) + } + } + +} diff --git a/LayoutKitSampleApp/MenuViewController.swift b/LayoutKitSampleApp/MenuViewController.swift index 704eba25..96ba1e1c 100644 --- a/LayoutKitSampleApp/MenuViewController.swift +++ b/LayoutKitSampleApp/MenuViewController.swift @@ -19,6 +19,7 @@ class MenuViewController: UITableViewController { FeedCollectionViewController.self, FeedTableViewController.self, StackViewController.self, + AutoRotateStackViewController.self, NestedCollectionViewController.self, ForegroundMiniProfileViewController.self, BackgroundMiniProfileViewController.self, diff --git a/LayoutKitTests/RotatingStackTests.swift b/LayoutKitTests/RotatingStackTests.swift new file mode 100644 index 00000000..aa96b8a3 --- /dev/null +++ b/LayoutKitTests/RotatingStackTests.swift @@ -0,0 +1,44 @@ +// Copyright 2018 LinkedIn Corp. +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import XCTest +@testable import LayoutKit + +class RotatingStackTests: XCTestCase { + + func testVerticalAlignment() { + let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 64.0)) + let stack = AutoRotateStackLayout( + sublayouts: [ + ButtonLayout(type: .system, title: "Hello World! "), + ButtonLayout(type: .system, title: "Hello One! "), + ButtonLayout(type: .system, title: "Hello Two! "), + ButtonLayout(type: .system, title: "Hello Three! "), + ButtonLayout(type: .system, title: "Hello Four! "), + ButtonLayout(type: .system, title: "Hello Five! "), + ButtonLayout(type: .system, title: "Hello All! ") + ]) + stack.arrangement(width: view.frame.width).makeViews(in: view) + + // Since `StackLayout` do not conform to `Equatable`, we are checking equality of the object (pointers). + XCTAssertTrue(stack.stackLayoutToUse === stack.verticalStackLayout) + } + + func testHorizontalAlignment() { + let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 64.0)) + let stack = AutoRotateStackLayout( + sublayouts: [ + ButtonLayout(type: .system, title: "Hello World! "), + ButtonLayout(type: .system, title: "Hello All! ") + ]) + stack.arrangement(width: view.frame.width).makeViews(in: view) + + // Since `StackLayout` do not conform to `Equatable`, we are checking equality of the object (pointers). + XCTAssertTrue(stack.stackLayoutToUse === stack.horizontalStackLayout) + } +} diff --git a/Sources/Layouts/AutoRotateStackLayout.swift b/Sources/Layouts/AutoRotateStackLayout.swift new file mode 100644 index 00000000..3d0d27e3 --- /dev/null +++ b/Sources/Layouts/AutoRotateStackLayout.swift @@ -0,0 +1,68 @@ +// Copyright 2018 LinkedIn Corp. +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import UIKit + +/** + A layout that stacks sublayouts along the horizontal axis by default. + If there is not enough space available along the horizontal axis, then it stacks all sublayouts along the vertical axis. + */ +open class AutoRotateStackLayout: BaseLayout { + + let horizontalStackLayout: StackLayout + let verticalStackLayout: StackLayout + + var stackLayoutToUse: StackLayout + + public init(spacing: CGFloat = 0, + distribution: StackLayoutDistribution = .fillFlexing, + alignment: Alignment = .fill, + flexibility: Flexibility = .inflexible, + viewReuseId: String? = nil, + sublayouts: [Layout], + config: ((V) -> Void)? = nil) { + horizontalStackLayout = StackLayout( + axis: .horizontal, + spacing: spacing, + distribution: distribution, + alignment: alignment, + flexibility: flexibility, + viewReuseId: viewReuseId, + sublayouts: sublayouts, + config: config) + + verticalStackLayout = StackLayout( + axis: .vertical, + spacing: spacing, + distribution: distribution, + alignment: alignment, + flexibility: flexibility, + viewReuseId: viewReuseId, + sublayouts: sublayouts, + config: config) + + stackLayoutToUse = horizontalStackLayout + + super.init(alignment: alignment, flexibility: flexibility, viewReuseId: viewReuseId, config: config) + } +} + +extension AutoRotateStackLayout: ConfigurableLayout { + + public func measurement(within maxSize: CGSize) -> LayoutMeasurement { + // Prioritize using the StackLayout with .horizontal axis. Use StackLayout with .vertical axis only if the horizontal StackView does not fit in the available width. + let horizontalStackMeasurement = horizontalStackLayout.measurement(within: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)) + stackLayoutToUse = horizontalStackMeasurement.size.width > maxSize.width ? verticalStackLayout : horizontalStackLayout + + return stackLayoutToUse.measurement(within: maxSize) + } + + public func arrangement(within rect: CGRect, measurement: LayoutMeasurement) -> LayoutArrangement { + return stackLayoutToUse.arrangement(within: rect, measurement: measurement) + } +}