├── .gitignore ├── .swift-version ├── .travis.yml ├── CONTRIBUTING.md ├── ExampleLayouts ├── CircleImagePileLayout.swift ├── DividerStackLayout.swift ├── ExampleLayouts.h ├── FeedItemLayout.swift ├── FixedWidthCellCollectionViewLayout.swift ├── HelloWorldAutoLayoutView.swift ├── HelloWorldLayout.swift ├── Info.plist ├── MiniProfileLayout.swift ├── ProfileCardLayout.swift └── SkillsCardLayout.swift ├── LICENSE ├── LayoutKit-iOS copy-Info.plist ├── LayoutKit.playground ├── Pages │ ├── Animation.xcplaygroundpage │ │ └── Contents.swift │ ├── Button.xcplaygroundpage │ │ └── Contents.swift │ ├── Counter.xcplaygroundpage │ │ └── Contents.swift │ ├── Inset.xcplaygroundpage │ │ └── Contents.swift │ ├── Introduction.xcplaygroundpage │ │ └── Contents.swift │ ├── Label.xcplaygroundpage │ │ └── Contents.swift │ ├── Overlay.xcplaygroundpage │ │ └── Contents.swift │ ├── Size.xcplaygroundpage │ │ └── Contents.swift │ ├── Stack.xcplaygroundpage │ │ └── Contents.swift │ ├── Test.xcplaygroundpage │ │ └── Contents.swift │ └── TextView.xcplaygroundpage │ │ └── Contents.swift ├── Resources │ └── earth.png ├── Sources │ ├── CocoaTarget.swift │ ├── Debug.swift │ ├── UIControl+Handler.swift │ └── UIImage+Resize.swift └── contents.xcplayground ├── LayoutKit.podspec ├── LayoutKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── ExampleLayouts-iOS.xcscheme │ ├── LayoutKit-iOS.xcscheme │ ├── LayoutKit-macOS.xcscheme │ ├── LayoutKit-tvOS.xcscheme │ ├── LayoutKitObjC-iOS.xcscheme │ └── LayoutKitSampleApp.xcscheme ├── LayoutKitObjC.podspec ├── LayoutKitObjCSampleApp ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── Launch Screen.storyboard ├── RotationLayout.h ├── RotationLayout.m ├── ViewController.h ├── ViewController.m └── main.m ├── LayoutKitSampleApp ├── AppDelegate.swift ├── Assets.xcassets │ ├── 20x20.imageset │ │ ├── 20x20.png │ │ └── Contents.json │ ├── 350x200.imageset │ │ ├── 350x200.png │ │ └── Contents.json │ ├── 50x50.imageset │ │ ├── 50x50.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── nick.imageset │ │ ├── Contents.json │ │ └── nick.jpg │ └── sergei.imageset │ │ ├── Contents.json │ │ └── sergei.jpg ├── BackgroundMiniProfileViewController.swift ├── BatchUpdatesBaseViewController.swift ├── BatchUpdatesCollectionViewController.swift ├── BatchUpdatesTableViewController.swift ├── Benchmarks │ ├── BenchmarkViewController.swift │ ├── CollectionViewController.swift │ ├── DataBinder.swift │ ├── FeedItemAutoLayoutView.swift │ ├── FeedItemData.swift │ ├── FeedItemLayoutKitView.swift │ ├── FeedItemManualView.swift │ ├── FeedItemUIStackView.swift │ ├── Stopwatch.swift │ └── TableViewController.swift ├── DWURecyclingAlert.m ├── FeedBaseViewController.swift ├── FeedCollectionViewController.swift ├── FeedScrollViewController.swift ├── FeedTableViewController.swift ├── ForegroundProfileViewController.swift ├── Info.plist ├── LabledImageLayout.swift ├── LaunchScreen.xib ├── MenuViewController.swift ├── NestedCollectionViewController.swift ├── OverlayViewController.swift ├── StackViewController.swift └── UrlImageLayout.swift ├── LayoutKitTests ├── AlignmentTests.swift ├── ButtonLayoutTests.swift ├── CGFloatExtensionTests.swift ├── CollectionExtension.swift ├── CollectionViewTests.swift ├── DensityAssertions.swift ├── EmbeddedLayoutTests.swift ├── IndexSetExtension.swift ├── Info.plist ├── InsetLayoutTests.swift ├── LabelLayoutTests.swift ├── LayoutArrangementTests.swift ├── OverlayLayoutTests.swift ├── ReloadableViewLayoutAdapterCollectionViewOverrideTests.swift ├── ReloadableViewLayoutAdapterCollectionViewTests.swift ├── ReloadableViewLayoutAdapterTableViewOverrideTests.swift ├── ReloadableViewLayoutAdapterTableViewTests.swift ├── ReloadableViewLayoutAdapterTestCase.swift ├── ReloadableViewTests.swift ├── SizeLayoutTests.swift ├── StackLayoutDistributionTests.swift ├── StackLayoutFlexibilityTests.swift ├── StackLayoutSpacingTests.swift ├── StackLayoutTests.swift ├── StackViewTests.swift ├── TableViewTests.swift ├── TestStack.swift ├── TextExtension.swift ├── TextViewLayoutTests.swift ├── UIFontExtension.swift └── ViewRecyclerTests.swift ├── NOTICE ├── Package.swift ├── README.md ├── RELEASE-CHECKLIST.md ├── Sources ├── Alignment.swift ├── Animation.swift ├── AppKitSupport.swift ├── Axis.swift ├── ConfigurableLayout.swift ├── Flexibility.swift ├── Info.plist ├── Internal │ ├── CFAbsoluteTimeExtension.swift │ ├── CGFloatExtension.swift │ ├── CGSizeExtension.swift │ ├── NSAttributedStringExtension.swift │ └── TextViewDefaultFont.swift ├── Layout.swift ├── LayoutArrangement.swift ├── LayoutKit.h ├── LayoutKitObjC.h ├── LayoutMeasurement.swift ├── Layouts │ ├── BaseLayout.swift │ ├── ButtonLayout.swift │ ├── InsetLayout.swift │ ├── LabelLayout.swift │ ├── OverlayLayout.swift │ ├── SizeLayout.swift │ ├── StackLayout.swift │ └── TextViewLayout.swift ├── Math │ ├── AxisFlexibility.swift │ ├── AxisPoint.swift │ └── AxisSize.swift ├── ObjCSupport │ ├── Builders │ │ ├── LOKButtonLayoutBuilder.h │ │ ├── LOKButtonLayoutBuilder.m │ │ ├── LOKInsetLayoutBuilder.h │ │ ├── LOKInsetLayoutBuilder.m │ │ ├── LOKLabelLayoutBuilder.h │ │ ├── LOKLabelLayoutBuilder.m │ │ ├── LOKLayoutBuilder.h │ │ ├── LOKOverlayLayoutBuilder.h │ │ ├── LOKOverlayLayoutBuilder.m │ │ ├── LOKSizeLayoutBuilder.h │ │ ├── LOKSizeLayoutBuilder.m │ │ ├── LOKStackLayoutBuilder.h │ │ ├── LOKStackLayoutBuilder.m │ │ ├── LOKTextViewLayoutBuilder.h │ │ └── LOKTextViewLayoutBuilder.m │ ├── Internal │ │ ├── ReverseWrappedLayout.swift │ │ └── WrappedLayout.swift │ ├── LOKAlignment.swift │ ├── LOKAnimation.swift │ ├── LOKBaseLayout.swift │ ├── LOKBatchUpdates.swift │ ├── LOKButtonLayout.swift │ ├── LOKButtonLayoutType.swift │ ├── LOKFlexibility.swift │ ├── LOKInsetLayout.swift │ ├── LOKLabelLayout.swift │ ├── LOKLayout.swift │ ├── LOKLayoutArrangement.swift │ ├── LOKLayoutArrangementSection.swift │ ├── LOKLayoutMeasurement.swift │ ├── LOKLayoutSection.swift │ ├── LOKOverlayLayout.swift │ ├── LOKReloadableViewLayoutAdapter.swift │ ├── LOKSizeLayout.swift │ ├── LOKStackLayout.swift │ └── LOKTextViewLayout.swift ├── Text.swift ├── UIKitSupport.swift ├── ViewRecycler.swift └── Views │ ├── BatchUpdates.swift │ ├── LayoutAdapterCollectionView.swift │ ├── LayoutAdapterTableView.swift │ ├── ReloadableView.swift │ ├── ReloadableViewLayoutAdapter+UICollectionView.swift │ ├── ReloadableViewLayoutAdapter+UITableView.swift │ ├── ReloadableViewLayoutAdapter.swift │ ├── ReloadableViewUpdateManager.swift │ └── StackView.swift ├── Tests ├── cocoapods │ ├── ios │ │ ├── LayoutKit-iOS.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ └── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── LayoutKit-iOS.xcscheme │ │ ├── LayoutKit-iOS │ │ │ ├── AppDelegate.swift │ │ │ └── Info.plist │ │ └── Podfile │ ├── macos │ │ ├── LayoutKit-macOS.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ └── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── LayoutKit-macOS.xcscheme │ │ ├── LayoutKit-macOS │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ └── Info.plist │ │ └── Podfile │ └── tvos │ │ ├── LayoutKit-tvOS.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── LayoutKit-tvOS.xcscheme │ │ ├── LayoutKit-tvOS │ │ ├── AppDelegate.swift │ │ └── Info.plist │ │ └── Podfile └── swift-package-manager │ └── ios │ ├── LayoutKitSampleApp.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── LayoutKitSampleApp.xcscheme │ └── Podfile ├── build.sh ├── debug-time-function-bodies.sh ├── docs ├── CNAME ├── animations.md ├── benchmarks.md ├── building-ui.md ├── code-documentation.md ├── custom-layouts.md ├── img │ ├── animation-example.gif │ ├── collectionview-benchmark.png │ ├── helloworld.png │ ├── layoutkit.svg │ ├── logo.png │ ├── logo.svg │ ├── nick.png │ ├── sergei.png │ └── tableview-benchmark.png ├── index.md ├── layoutkit.css └── uikit.md └── mkdocs.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Directory 2 | build/ 3 | DerivedData/ 4 | build.log 5 | 6 | # User Data 7 | *.xctimeline 8 | xcuserdata 9 | 10 | # mkdocs 11 | site/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.2 3 | script: sh build.sh /tmp/LayoutKit 4 | after_success: 5 | - bash <(curl -s https://codecov.io/bash) -D /tmp/LayoutKit 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to LayoutKit 2 | 3 | Thanks for helping make LayoutKit better! Here are some guidelines to follow when contributing. 4 | 5 | ## Reporting security issues 6 | 7 | Please report security issues to [security@linkedin.com](mailto:security@linkedin.com) with a subject line of this format: 8 | 9 | `GitHub linkedin/LayoutKit - [summary of issue]` 10 | 11 | ## Bugs 12 | 13 | Please [create an issue](https://github.com/linkedin/LayoutKit/issues/new) and include enough information to reproduce the issue you are seeing. 14 | A snippet of code that works in a Swift playground is ideal! 15 | 16 | ## Feature requests 17 | 18 | Please [create an issue](https://github.com/linkedin/LayoutKit/issues/new) to describe the feature and why you think it would be useful. 19 | 20 | ## Pull requests 21 | 22 | Pull requests should: 23 | - Build and pass all tests. 24 | - Have tests for all new code. 25 | - Follow the [Swift API design guidelines](https://swift.org/documentation/api-design-guidelines/). 26 | -------------------------------------------------------------------------------- /ExampleLayouts/CircleImagePileLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /// Displays a pile of overlapping circular images. 13 | open class CircleImagePileLayout: StackLayout { 14 | 15 | public enum Mode { 16 | case leadingOnTop, trailingOnTop 17 | } 18 | 19 | public let mode: Mode 20 | 21 | public init(imageNames: [String], mode: Mode = .trailingOnTop, alignment: Alignment = .topLeading, viewReuseId: String? = nil) { 22 | self.mode = mode 23 | let sublayouts: [Layout] = imageNames.map { imageName in 24 | return SizeLayout(width: 50, height: 50, config: { imageView in 25 | imageView.image = UIImage(named: imageName) 26 | imageView.layer.cornerRadius = 25 27 | imageView.layer.masksToBounds = true 28 | imageView.layer.borderColor = UIColor.white.cgColor 29 | imageView.layer.borderWidth = 2 30 | }) 31 | } 32 | super.init( 33 | axis: .horizontal, 34 | spacing: -25, 35 | distribution: .leading, 36 | alignment: alignment, 37 | flexibility: .inflexible, 38 | viewReuseId: viewReuseId, 39 | sublayouts: sublayouts) 40 | } 41 | 42 | open override var needsView: Bool { 43 | return super.needsView || mode == .leadingOnTop 44 | } 45 | } 46 | 47 | open class CircleImagePileView: UIView { 48 | 49 | open override func addSubview(_ view: UIView) { 50 | // Make sure views are inserted below existing views so that the first image in the face pile is on top. 51 | if let lastSubview = subviews.last { 52 | insertSubview(view, belowSubview: lastSubview) 53 | } else { 54 | super.addSubview(view) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ExampleLayouts/DividerStackLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /** 13 | A layout that places a divider view in the spacing between the stack's sublayouts. 14 | */ 15 | open class DividerStackLayout: StackLayout { 16 | 17 | public init(stack: StackLayout, dividerConfig: ((DividerView) -> Void)?) { 18 | let sublayouts: [Layout] 19 | if stack.spacing > 0 { 20 | var dividedSublayouts = [Layout]() 21 | let size = AxisSize(axis: stack.axis, axisLength: stack.spacing, crossLength: 0).size 22 | let divider = SizeLayout(size: size, alignment: .fill, flexibility: .flexible, config: dividerConfig) 23 | for (index, sublayout) in stack.sublayouts.enumerated() { 24 | dividedSublayouts.append(sublayout) 25 | if index != stack.sublayouts.count - 1 { 26 | dividedSublayouts.append(divider) 27 | } 28 | } 29 | sublayouts = dividedSublayouts 30 | } else { 31 | sublayouts = stack.sublayouts 32 | } 33 | 34 | super.init(axis: stack.axis, 35 | spacing: 0, 36 | distribution: stack.distribution, 37 | alignment: stack.alignment, 38 | flexibility: stack.flexibility, 39 | sublayouts: sublayouts, 40 | config: stack.config) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ExampleLayouts/ExampleLayouts.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleLayouts.h 3 | // ExampleLayouts 4 | // 5 | // Created by Nick Snyder on 9/12/16. 6 | // 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ExampleLayouts. 12 | FOUNDATION_EXPORT double ExampleLayoutsVersionNumber; 13 | 14 | //! Project version string for ExampleLayouts. 15 | FOUNDATION_EXPORT const unsigned char ExampleLayoutsVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /ExampleLayouts/HelloWorldAutoLayoutView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | /** 12 | A simple hello world layout that uses Auto Layout. 13 | Compare to HelloWorldLayout.swift 14 | */ 15 | open class HelloWorldAutoLayoutView: UIView { 16 | 17 | private lazy var imageView: UIImageView = { 18 | let imageView = UIImageView() 19 | imageView.translatesAutoresizingMaskIntoConstraints = false 20 | imageView.setContentHuggingPriority(UILayoutPriority.required, for: .vertical) 21 | imageView.setContentHuggingPriority(UILayoutPriority.required, for: .horizontal) 22 | imageView.setContentCompressionResistancePriority(UILayoutPriority.required, for: .vertical) 23 | imageView.setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal) 24 | imageView.image = UIImage(named: "earth.png") 25 | return imageView 26 | }() 27 | 28 | private lazy var label: UILabel = { 29 | let label = UILabel() 30 | label.translatesAutoresizingMaskIntoConstraints = false 31 | label.text = "Hello World!" 32 | return label 33 | }() 34 | 35 | public override init(frame: CGRect) { 36 | super.init(frame: frame) 37 | addSubview(imageView) 38 | addSubview(label) 39 | 40 | let views: [String: UIView] = ["imageView": imageView, "label": label] 41 | var constraints = [NSLayoutConstraint]() 42 | constraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-4-[imageView(==50)]-4-|", options: [], metrics: nil, views: views)) 43 | constraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-4-[imageView(==50)]-4-[label]-8-|", options: [], metrics: nil, views: views)) 44 | constraints.append(NSLayoutConstraint(item: label, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0)) 45 | NSLayoutConstraint.activate(constraints) 46 | } 47 | 48 | public required init?(coder aDecoder: NSCoder) { 49 | fatalError("init(coder:) has not been implemented") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ExampleLayouts/HelloWorldLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /** 13 | A simple hello world layout. 14 | 15 | ``` 16 | let helloView = HelloWorldLayout().arrangement().makeViews() 17 | ``` 18 | 19 | Compare to HelloWorldAutoLayout.swift 20 | */ 21 | open class HelloWorldLayout: InsetLayout { 22 | 23 | public init(text: String = "Hello World!") { 24 | super.init( 25 | insets: EdgeInsets(top: 4, left: 4, bottom: 4, right: 8), 26 | sublayout: StackLayout( 27 | axis: .horizontal, 28 | spacing: 4, 29 | sublayouts: [ 30 | SizeLayout(width: 50, height: 50, config: { imageView in 31 | imageView.image = UIImage(named: "earth.png") 32 | }), 33 | LabelLayout(text: text, alignment: .center) 34 | ] 35 | ) 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ExampleLayouts/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ExampleLayouts/MiniProfileLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | // ANY CHANGES TO THIS FILE SHOULD ALSO BE MADE IN `Documentation/docs/layouts.md` 10 | 11 | import UIKit 12 | import LayoutKit 13 | 14 | /// A small version of a LinkedIn profile. 15 | open class MiniProfileLayout: InsetLayout { 16 | 17 | public init(imageName: String, name: String, headline: String) { 18 | let image = SizeLayout( 19 | width: 80, 20 | height: 80, 21 | alignment: .center, 22 | config: { imageView in 23 | imageView.image = UIImage(named: imageName) 24 | 25 | // Not the most performant way to do a corner radius, but this is just a demo. 26 | imageView.layer.cornerRadius = 40 27 | imageView.layer.masksToBounds = true 28 | } 29 | ) 30 | 31 | let nameLayout = LabelLayout(text: name, font: UIFont.systemFont(ofSize: 40)) 32 | 33 | let headlineLayout = LabelLayout( 34 | text: headline, 35 | font: UIFont.systemFont(ofSize: 20), 36 | config: { label in 37 | label.textColor = UIColor.darkGray 38 | } 39 | ) 40 | 41 | super.init( 42 | insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), 43 | sublayout: StackLayout( 44 | axis: .horizontal, 45 | spacing: 8, 46 | sublayouts: [ 47 | image, 48 | StackLayout(axis: .vertical, spacing: 2, sublayouts: [nameLayout, headlineLayout]) 49 | ] 50 | ), 51 | config: { view in 52 | view.backgroundColor = UIColor.white 53 | } 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ExampleLayouts/ProfileCardLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | open class ProfileCardLayout: StackLayout { 13 | 14 | public init(name: String, connectionDegree: String, headline: String, timestamp: String, profileImageName: String) { 15 | let labelConfig = { (label: UILabel) in 16 | label.backgroundColor = UIColor.yellow 17 | } 18 | 19 | let nameAndConnectionDegree = StackLayout( 20 | axis: .horizontal, 21 | spacing: 4, 22 | sublayouts: [ 23 | LabelLayout(text: name, viewReuseId: "name", config: labelConfig), 24 | LabelLayout(text: connectionDegree, viewReuseId: "connectionDegree", config: { label in 25 | label.backgroundColor = UIColor.gray 26 | }), 27 | ] 28 | ) 29 | 30 | let headline = LabelLayout(text: headline, numberOfLines: 2, viewReuseId: "headline", config: labelConfig) 31 | let timestamp = LabelLayout(text: timestamp, numberOfLines: 2, viewReuseId: "timestamp", config: labelConfig) 32 | 33 | let verticalLabelStack = StackLayout( 34 | axis: .vertical, 35 | spacing: 2, 36 | alignment: Alignment(vertical: .center, horizontal: .leading), 37 | sublayouts: [nameAndConnectionDegree, headline, timestamp] 38 | ) 39 | 40 | let profileImage = SizeLayout( 41 | size: CGSize(width: 50, height: 50), 42 | viewReuseId: "profileImage", 43 | config: { imageView in 44 | imageView.image = UIImage(named: profileImageName) 45 | } 46 | ) 47 | 48 | super.init( 49 | axis: .horizontal, 50 | spacing: 4, 51 | sublayouts: [ 52 | profileImage, 53 | verticalLabelStack 54 | ] 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ExampleLayouts/SkillsCardLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | open class SkillsCardLayout: InsetLayout { 13 | 14 | public init(skill: String, endorsementCount: String, endorserProfileImageName: String) { 15 | let skillLabel = LabelLayout( 16 | text: skill, 17 | alignment: Alignment.center, 18 | flexibility: Flexibility.high, // Higher than default flexibility 19 | config: { label in 20 | label.backgroundColor = UIColor.yellow 21 | } 22 | ) 23 | let countLabel = LabelLayout( 24 | text: endorsementCount, 25 | alignment: Alignment.center, 26 | flexibility: Flexibility.low, 27 | config: { label in 28 | label.backgroundColor = UIColor.yellow 29 | } 30 | ) 31 | let endorserImage = SizeLayout( 32 | size: CGSize(width: 20, height: 20), 33 | alignment: Alignment.center, 34 | config: { imageView in 35 | imageView.image = UIImage(named: endorserProfileImageName) 36 | } 37 | ) 38 | let layout = StackLayout( 39 | axis: .horizontal, 40 | spacing: 8, 41 | distribution: .fillFlexing, 42 | alignment: Alignment(vertical: .center, horizontal: .fill), 43 | sublayouts: [ 44 | skillLabel, 45 | countLabel, 46 | endorserImage, 47 | ] 48 | ) 49 | super.init(insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), sublayout: layout) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LayoutKit-iOS copy-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 7.0.2 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LayoutKit.playground/Pages/Button.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PlaygroundSupport 3 | import LayoutKit 4 | 5 | let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) 6 | rootView.backgroundColor = .white 7 | 8 | func buttonLayout(_ text: String) -> ButtonLayout { 9 | return ButtonLayout( 10 | type: .custom, 11 | title: text, 12 | image: .image(UIImage(named: "earth")?.resize(width: 100)), 13 | font: .systemFont(ofSize: 36), 14 | contentEdgeInsets: .zero, 15 | alignment: .center, 16 | viewReuseId: "button", 17 | config: { 18 | $0.titleLabel?.backgroundColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1) 19 | $0.setTitleColor(#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1), for: .normal) 20 | $0.addHandler(for: .touchUpInside) { _ in 21 | print("Button tapped") 22 | } 23 | } 24 | ) 25 | } 26 | 27 | //let arrangement = buttonLayout("Hello World").arrangement(width: 50) 28 | let arrangement = buttonLayout("Tap me!").arrangement() 29 | print(arrangement) 30 | 31 | arrangement.makeViews(in: rootView) 32 | 33 | Debug.addBorderColorsRecursively(rootView) 34 | Debug.printRecursiveDescription(rootView) 35 | 36 | PlaygroundPage.current.liveView = rootView 37 | -------------------------------------------------------------------------------- /LayoutKit.playground/Pages/Inset.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PlaygroundSupport 3 | import LayoutKit 4 | 5 | let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) 6 | rootView.backgroundColor = .white 7 | 8 | func sizeLayout() -> SizeLayout { 9 | return SizeLayout( 10 | width: 280, 11 | height: 100, 12 | config: { 13 | $0.backgroundColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1) 14 | } 15 | ) 16 | } 17 | 18 | func insetLayout() -> InsetLayout { 19 | return InsetLayout( 20 | insets: UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20), 21 | alignment: .center, 22 | sublayout: sizeLayout(), 23 | config: { _ in } 24 | ) 25 | } 26 | 27 | //let arrangement = insetLayout().arrangement(width: 100, height: 150) 28 | let arrangement = insetLayout().arrangement() 29 | print(arrangement) 30 | 31 | arrangement.makeViews(in: rootView) 32 | 33 | Debug.addBorderColorsRecursively(rootView) 34 | Debug.printRecursiveDescription(rootView) 35 | 36 | PlaygroundPage.current.liveView = rootView 37 | -------------------------------------------------------------------------------- /LayoutKit.playground/Pages/Introduction.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import PlaygroundSupport 11 | import LayoutKit 12 | import ExampleLayouts 13 | 14 | // REMINDER: you need to manually build ExampleLayouts on the simulator for changes to be reflected in this playground. 15 | 16 | let helloWorld = HelloWorldLayout() 17 | 18 | helloWorld.arrangement().makeViews() 19 | 20 | helloWorld.arrangement(width: 250).makeViews() 21 | 22 | helloWorld.arrangement().makeViews(direction: .rightToLeft) // just for testing; RTL happens automatically for RTL languages. 23 | 24 | -------------------------------------------------------------------------------- /LayoutKit.playground/Pages/Label.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PlaygroundSupport 3 | import LayoutKit 4 | 5 | let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) 6 | rootView.backgroundColor = .white 7 | 8 | func labelLayout(_ text: String) -> LabelLayout { 9 | return LabelLayout( 10 | text: text, 11 | font: .systemFont(ofSize: 18), 12 | numberOfLines: 0, 13 | alignment: .center, 14 | flexibility: .high, 15 | viewReuseId: "label", 16 | config: { _ in } 17 | ) 18 | } 19 | 20 | //let arrangement = labelLayout("Hello World").arrangement(width: 100, height: 150) 21 | let arrangement = labelLayout("Hello World").arrangement() 22 | print(arrangement) 23 | 24 | arrangement.makeViews(in: rootView) 25 | 26 | Debug.addBorderColorsRecursively(rootView) 27 | Debug.printRecursiveDescription(rootView) 28 | 29 | PlaygroundPage.current.liveView = rootView 30 | -------------------------------------------------------------------------------- /LayoutKit.playground/Pages/Overlay.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PlaygroundSupport 3 | import LayoutKit 4 | 5 | /** 6 | This page demonstrates a complex layout that uses `OverlayLayout`, so that 7 | you can see the effect of various alignment values and how composing views 8 | together works. There's an orange view that the layout sits in. The layout 9 | is bordered by a white border & has a black background. The semi-transparent 10 | text is the base layout, centered within the overlay layout. The overlay 11 | layout is set to fill leading space, so it doesn't span the full view. There's 12 | a red & a green box that overlay the base layout, and a purple and brown box 13 | that underlay it. They're aligned in various ways, but the sizing is such that 14 | you can see how they're composed. Try changing the alignment values and sizes 15 | to see how that affects the outcome! 16 | */ 17 | 18 | let baseLayout = TextViewLayout( 19 | text: "This is the base layout\nAnd it's a bunch of text!", 20 | textContainerInset: UIEdgeInsets(top: 60, left: 60, bottom: 60, right: 60), 21 | layoutAlignment: .center, 22 | config: { textView in 23 | textView.textColor = .white 24 | textView.backgroundColor = UIColor(white: 1.0, alpha: 0.5) 25 | }) 26 | let greenBoxLayout = SizeLayout( 27 | width: 50, 28 | height: 80, 29 | alignment: .centerTrailing, 30 | config: { view in 31 | view.backgroundColor = .green 32 | }) 33 | let redBoxLayout = SizeLayout( 34 | width: 70, 35 | height: 90, 36 | alignment: .topLeading, 37 | config: { view in 38 | view.backgroundColor = .red 39 | }) 40 | let brownBoxLayout = SizeLayout( 41 | width: 160, 42 | height: 130, 43 | alignment: .bottomCenter, 44 | config: { view in 45 | view.backgroundColor = UIColor.brown 46 | }) 47 | let purpleBoxLayout = SizeLayout( 48 | width: 80, 49 | height: 550, 50 | alignment: .fillTrailing, 51 | config: { view in 52 | view.backgroundColor = .purple 53 | }) 54 | 55 | // Compose the layouts into an overlay layout 56 | let overlayLayout = OverlayLayout( 57 | primaryLayouts: [baseLayout], 58 | backgroundLayouts: [purpleBoxLayout, brownBoxLayout], 59 | overlayLayouts: [redBoxLayout, greenBoxLayout], 60 | alignment: .fillLeading, 61 | config: { overlayView in 62 | overlayView.backgroundColor = .black 63 | overlayView.layer.borderWidth = 2.0 64 | overlayView.layer.borderColor = UIColor.white.cgColor 65 | }) 66 | 67 | 68 | // Root view 69 | let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 70 | rootView.backgroundColor = .orange 71 | 72 | // Create arrangement within root view 73 | let arrangement = overlayLayout.arrangement( 74 | origin: .zero, 75 | width: rootView.bounds.width, 76 | height: rootView.bounds.height) 77 | print(arrangement) 78 | arrangement.makeViews(in: rootView) 79 | Debug.printRecursiveDescription(rootView) 80 | PlaygroundPage.current.liveView = rootView 81 | -------------------------------------------------------------------------------- /LayoutKit.playground/Pages/Size.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PlaygroundSupport 3 | import LayoutKit 4 | 5 | let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) 6 | rootView.backgroundColor = .white 7 | 8 | func sizeLayout() -> SizeLayout { 9 | return SizeLayout( 10 | width: 280, 11 | height: 100, 12 | config: { 13 | $0.backgroundColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1) 14 | } 15 | ) 16 | } 17 | 18 | //let arrangement = sizeLayout("Hello World").arrangement(width: 100, height: 100) 19 | let arrangement = sizeLayout().arrangement() 20 | print(arrangement) 21 | 22 | arrangement.makeViews(in: rootView) 23 | 24 | Debug.addBorderColorsRecursively(rootView) 25 | Debug.printRecursiveDescription(rootView) 26 | 27 | PlaygroundPage.current.liveView = rootView 28 | -------------------------------------------------------------------------------- /LayoutKit.playground/Pages/Stack.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PlaygroundSupport 3 | import LayoutKit 4 | 5 | let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) 6 | rootView.backgroundColor = .white 7 | 8 | func stackLayout(_ text: String, _ distribution: StackLayoutDistribution) -> StackLayout { 9 | return StackLayout( 10 | axis: .horizontal, 11 | spacing: 10, 12 | distribution: distribution, 13 | sublayouts: [ 14 | SizeLayout( 15 | width: 20, 16 | height: 40, 17 | alignment: .bottomFill, 18 | flexibility: .low, 19 | config: { 20 | $0.backgroundColor = .yellow 21 | } 22 | ), 23 | SizeLayout( 24 | width: 20, 25 | height: 10, 26 | alignment: .bottomFill, 27 | flexibility: .high, 28 | config: { 29 | $0.backgroundColor = .orange 30 | } 31 | ), 32 | SizeLayout( 33 | width: 50, 34 | height: 50, 35 | alignment: .center, 36 | config: { 37 | $0.image = UIImage(named: "earth") 38 | } 39 | ), 40 | LabelLayout( 41 | text: text, 42 | alignment: .center 43 | ) 44 | ] 45 | ) 46 | } 47 | 48 | func sizeLayout(_ text: String) -> SizeLayout { 49 | return SizeLayout( 50 | width: 280, 51 | height: 100, 52 | sublayout: stackLayout(text, .fillFlexing), 53 | config: { _ in } 54 | ) 55 | } 56 | 57 | //let arrangement = stackLayout("Hello World").arrangement(width: 100, height: 150) 58 | let arrangement = sizeLayout("Hello World").arrangement() 59 | print(arrangement) 60 | 61 | arrangement.makeViews(in: rootView) 62 | 63 | Debug.addBorderColorsRecursively(rootView) 64 | Debug.printRecursiveDescription(rootView) 65 | 66 | PlaygroundPage.current.liveView = rootView 67 | -------------------------------------------------------------------------------- /LayoutKit.playground/Pages/TextView.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PlaygroundSupport 3 | import LayoutKit 4 | 5 | let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480)) 6 | rootView.backgroundColor = .white 7 | 8 | let textString = "Hello World\nHello World\nHello World\nHello World\nHello World\n" 9 | let attributedString1 = NSMutableAttributedString( 10 | string: textString, 11 | attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)]) 12 | let attributedString2 = NSMutableAttributedString( 13 | string: textString, 14 | attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12)]) 15 | attributedString1.append(attributedString2) 16 | let attributedText = Text.attributed(attributedString1) 17 | 18 | let textContainerInset = UIEdgeInsets(top: 2, left: 3, bottom: 4, right: 5) 19 | 20 | let textViewLayout = TextViewLayout(text: attributedText, textContainerInset: textContainerInset) 21 | let arrangement = textViewLayout.arrangement() 22 | arrangement.makeViews(in: rootView) 23 | 24 | Debug.addBorderColorsRecursively(rootView) 25 | Debug.printRecursiveDescription(rootView) 26 | 27 | PlaygroundPage.current.liveView = rootView 28 | -------------------------------------------------------------------------------- /LayoutKit.playground/Resources/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/LayoutKit.playground/Resources/earth.png -------------------------------------------------------------------------------- /LayoutKit.playground/Sources/CocoaTarget.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A target that accepts action messages. 4 | internal final class CocoaTarget: NSObject { 5 | private let action: (Value) -> () 6 | 7 | internal init(_ action: @escaping (Value) -> ()) { 8 | self.action = action 9 | } 10 | 11 | @objc 12 | internal func sendNext(_ receiver: Any?) { 13 | action(receiver as! Value) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LayoutKit.playground/Sources/Debug.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum Debug { 4 | 5 | public static func color(at index: Int, isVivid: Bool) -> UIColor { 6 | let hue = (CGFloat(index) * 0.618033988749895).truncatingRemainder(dividingBy: 1) 7 | let saturation: CGFloat = isVivid ? 0.9 : 0.1 8 | return UIColor(hue: hue, saturation: saturation, brightness: 1, alpha: 0.8) 9 | } 10 | 11 | public static func addBorderColorsRecursively(_ view: UIView) { 12 | var i = 0 13 | func _addBorderColorsRecursively(_ view: UIView) { 14 | view.layer.borderColor = color(at: i, isVivid: view.subviews.isEmpty).cgColor 15 | view.layer.borderWidth = 2 16 | 17 | for subview in view.subviews { 18 | i += 1 19 | _addBorderColorsRecursively(subview) 20 | } 21 | } 22 | _addBorderColorsRecursively(view) 23 | } 24 | 25 | public static func printRecursiveDescription(_ view: UIView) { 26 | print(view.perform(Selector(("recursiveDescription"))).takeUnretainedValue()) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /LayoutKit.playground/Sources/UIControl+Handler.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | private var controlHandlerKey: Int8 = 0 4 | 5 | extension UIControl { 6 | 7 | public func addHandler(for controlEvents: UIControl.Event, handler: @escaping (UIControl) -> ()) { 8 | if let oldTarget = objc_getAssociatedObject(self, &controlHandlerKey) as? CocoaTarget { 9 | self.removeTarget(oldTarget, action: #selector(oldTarget.sendNext), for: controlEvents) 10 | } 11 | 12 | let target = CocoaTarget(handler) 13 | objc_setAssociatedObject(self, &controlHandlerKey, target, .OBJC_ASSOCIATION_RETAIN) 14 | self.addTarget(target, action: #selector(target.sendNext), for: controlEvents) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /LayoutKit.playground/Sources/UIImage+Resize.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIImage { 4 | 5 | /// Resizing with preserving aspect-ratio. 6 | public func resize(width: CGFloat) -> UIImage { 7 | let image = self 8 | let scale = width / image.size.width 9 | let height = image.size.height * scale 10 | UIGraphicsBeginImageContext(CGSize(width: width, height: height)) 11 | image.draw(in: CGRect(x: 0, y: 0, width: width, height: height)) 12 | let newImage = UIGraphicsGetImageFromCurrentImageContext() 13 | UIGraphicsEndImageContext() 14 | 15 | return newImage! 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /LayoutKit.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LayoutKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'LayoutKit' 3 | spec.version = '10.1.0' 4 | spec.license = { :type => 'Apache License, Version 2.0' } 5 | spec.homepage = 'http://layoutkit.org' 6 | spec.authors = 'LinkedIn' 7 | spec.summary = 'LayoutKit is a fast view layout library for iOS, macOS, and tvOS.' 8 | spec.source = { :git => 'https://github.com/linkedin/LayoutKit.git', :tag => spec.version } 9 | spec.source_files = 'Sources/**/*.swift' 10 | spec.documentation_url = 'http://layoutkit.org' 11 | 12 | spec.ios.deployment_target = '8.0' 13 | spec.ios.frameworks = 'Foundation', 'CoreGraphics', 'UIKit' 14 | spec.ios.exclude_files = [ 15 | 'Sources/AppKitSupport.swift', 16 | 'Sources/ObjCSupport/**', 17 | 'Sources/ObjCSupport/Internal/**' 18 | ] 19 | 20 | spec.osx.deployment_target = '10.9' 21 | spec.osx.frameworks = 'Foundation', 'CoreGraphics', 'AppKit' 22 | spec.osx.exclude_files = [ 23 | 'Sources/Internal/CGFloatExtension.swift', 24 | 'Sources/Internal/TextViewDefaultFont.swift', 25 | 'Sources/Internal/NSAttributedStringExtension.swift', 26 | 'Sources/Layouts/ButtonLayout.swift', 27 | 'Sources/Layouts/LabelLayout.swift', 28 | 'Sources/Layouts/TextViewLayout.swift', 29 | 'Sources/ObjCSupport/**', 30 | 'Sources/ObjCSupport/Internal/**', 31 | 'Sources/Text.swift', 32 | 'Sources/UIKitSupport.swift', 33 | 'Sources/Views/**' 34 | ] 35 | 36 | spec.tvos.deployment_target = '9.0' 37 | spec.tvos.frameworks = 'Foundation', 'CoreGraphics', 'UIKit' 38 | spec.tvos.exclude_files = [ 39 | 'Sources/AppKitSupport.swift', 40 | 'Sources/ObjCSupport/**', 41 | 'Sources/ObjCSupport/Internal/**' 42 | ] 43 | 44 | end 45 | 46 | -------------------------------------------------------------------------------- /LayoutKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LayoutKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LayoutKitObjC.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'LayoutKitObjC' 3 | spec.version = '10.1.0' 4 | spec.license = { :type => 'Apache License, Version 2.0' } 5 | spec.homepage = 'http://layoutkit.org' 6 | spec.authors = 'LinkedIn' 7 | spec.summary = 'LayoutKit is a fast view layout library for iOS, macOS, and tvOS. Now with Objective-C support.' 8 | spec.source = { :git => 'https://github.com/linkedin/LayoutKit.git', :tag => spec.version } 9 | spec.source_files = 'Sources/**/*.{swift,h,m}' 10 | spec.documentation_url = 'http://layoutkit.org' 11 | 12 | spec.ios.deployment_target = '8.0' 13 | spec.ios.frameworks = 'Foundation', 'CoreGraphics', 'UIKit' 14 | spec.ios.exclude_files = 'Sources/AppKitSupport.swift' 15 | 16 | spec.osx.deployment_target = '10.9' 17 | spec.osx.frameworks = 'Foundation', 'CoreGraphics', 'AppKit' 18 | spec.osx.exclude_files = [ 19 | 'Sources/Internal/CGFloatExtension.swift', 20 | 'Sources/Internal/TextViewDefaultFont.swift', 21 | 'Sources/Internal/NSAttributedStringExtension.swift', 22 | 'Sources/Layouts/ButtonLayout.swift', 23 | 'Sources/Layouts/LabelLayout.swift', 24 | 'Sources/Layouts/TextViewLayout.swift', 25 | 'Sources/ObjCSupport/Builders/LOKButtonLayoutBuilder.*', 26 | 'Sources/ObjCSupport/Builders/LOKLabelLayoutBuilder.*', 27 | 'Sources/ObjCSupport/Builders/LOKTextViewLayoutBuilder.*', 28 | 'Sources/ObjCSupport/LOKBatchUpdates.swift', 29 | 'Sources/ObjCSupport/LOKButtonLayout.swift', 30 | 'Sources/ObjCSupport/LOKButtonLayoutType.swift', 31 | 'Sources/ObjCSupport/LOKLabelLayout.swift', 32 | 'Sources/ObjCSupport/LOKLayoutArrangementSection.swift', 33 | 'Sources/ObjCSupport/LOKLayoutSection.swift', 34 | 'Sources/ObjCSupport/LOKReloadableViewLayoutAdapter.swift', 35 | 'Sources/ObjCSupport/LOKTextViewLayout.swift', 36 | 'Sources/Text.swift', 37 | 'Sources/UIKitSupport.swift', 38 | 'Sources/Views/**' 39 | ] 40 | 41 | spec.tvos.deployment_target = '9.0' 42 | spec.tvos.frameworks = 'Foundation', 'CoreGraphics', 'UIKit' 43 | spec.tvos.exclude_files = [ 44 | 'Sources/AppKitSupport.swift', 45 | 46 | # Excluded due to "'systemFontSize' is unavailable" 47 | 'Sources/ObjCSupport/Builders/LOKLabelLayoutBuilder.*', 48 | 'Sources/ObjCSupport/LOKLabelLayout.swift' 49 | ] 50 | 51 | end 52 | 53 | -------------------------------------------------------------------------------- /LayoutKitObjCSampleApp/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /LayoutKitObjCSampleApp/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | 10 | #import "AppDelegate.h" 11 | #import "ViewController.h" 12 | 13 | @interface AppDelegate () 14 | 15 | @end 16 | 17 | @implementation AppDelegate 18 | 19 | 20 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 21 | // Override point for customization after application launch. 22 | self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; 23 | ViewController *viewController = [[ViewController alloc] init]; 24 | UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; 25 | self.window.rootViewController = navigationController; 26 | [self.window makeKeyAndVisible]; 27 | return YES; 28 | } 29 | 30 | 31 | - (void)applicationWillResignActive:(UIApplication *)application { 32 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 33 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 34 | } 35 | 36 | 37 | - (void)applicationDidEnterBackground:(UIApplication *)application { 38 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 39 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 40 | } 41 | 42 | 43 | - (void)applicationWillEnterForeground:(UIApplication *)application { 44 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 45 | } 46 | 47 | 48 | - (void)applicationDidBecomeActive:(UIApplication *)application { 49 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 50 | } 51 | 52 | 53 | - (void)applicationWillTerminate:(UIApplication *)application { 54 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 55 | } 56 | 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /LayoutKitObjCSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /LayoutKitObjCSampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | Launch Screen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /LayoutKitObjCSampleApp/Launch Screen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LayoutKitObjCSampleApp/RotationLayout.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | #import 10 | #import 11 | 12 | @interface RotationLayout: NSObject 13 | 14 | @property (nonatomic, nonnull, readonly) id sublayout; 15 | @property (nonatomic, nonnull, readonly) LOKAlignment *alignment; 16 | @property (nonatomic, nonnull, readonly) LOKFlexibility *flexibility; 17 | @property (nonatomic, copy, nullable, readonly) NSString *viewReuseId; 18 | @property (nonatomic, readonly) BOOL needsView; 19 | 20 | - (nonnull instancetype)initWithSublayout:(nonnull id)sublayout alignment:(nullable LOKAlignment *)alignment viewReuseId:(nullable NSString *)viewReuseId; 21 | 22 | @end 23 | 24 | -------------------------------------------------------------------------------- /LayoutKitObjCSampleApp/RotationLayout.m: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | #import 10 | #import 11 | 12 | #import "RotationLayout.h" 13 | 14 | @interface RotationView: UIView 15 | @end 16 | @implementation RotationView 17 | - (instancetype)init { 18 | if (self = [super init]) { 19 | self.transform = CGAffineTransformRotate(CGAffineTransformIdentity, M_PI_2); 20 | } 21 | return self; 22 | } 23 | @end 24 | 25 | @implementation RotationLayout 26 | 27 | - (instancetype)initWithSublayout:(id)sublayout alignment:(LOKAlignment *)alignment viewReuseId:(NSString *)viewReuseId { 28 | if (self = [super init]) { 29 | _sublayout = sublayout; 30 | _alignment = alignment; 31 | _needsView = YES; 32 | _flexibility = sublayout.flexibility; 33 | _viewReuseId = viewReuseId; 34 | } 35 | return self; 36 | } 37 | 38 | - (nonnull LOKLayoutArrangement *)arrangementWithin:(CGRect)rect measurement:(nonnull LOKLayoutMeasurement *)measurement { 39 | CGSize unrotatedSize = CGSizeMake(measurement.size.height, measurement.size.width); 40 | 41 | LOKLayoutArrangement *sublayoutArrangement = [self.sublayout arrangementWithin:CGRectMake(0, 0, unrotatedSize.width, unrotatedSize.height) 42 | measurement:measurement.sublayouts.firstObject]; 43 | CGRect frame = [self.alignment positionWithSize:measurement.size in:rect]; 44 | return [[LOKLayoutArrangement alloc] initWithLayout:self 45 | frame:frame 46 | sublayouts:@[sublayoutArrangement]]; 47 | } 48 | 49 | - (nonnull UIView *)makeView { 50 | return [[RotationView alloc] init]; 51 | } 52 | 53 | - (void)configureView:(UIView *)view { 54 | 55 | } 56 | 57 | - (nonnull LOKLayoutMeasurement *)measurementWithin:(CGSize)maxSize { 58 | LOKLayoutMeasurement *sublayoutMeasurement = [self.sublayout measurementWithin:maxSize]; 59 | CGSize rotatedSize = CGSizeMake(sublayoutMeasurement.size.height, sublayoutMeasurement.size.width); 60 | return [[LOKLayoutMeasurement alloc] initWithLayout:self 61 | size:rotatedSize 62 | maxSize:maxSize 63 | sublayouts:@[sublayoutMeasurement]]; 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /LayoutKitObjCSampleApp/ViewController.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /LayoutKitObjCSampleApp/main.m: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window?.rootViewController = UINavigationController(rootViewController: MenuViewController()) 20 | window?.makeKeyAndVisible() 21 | return true 22 | } 23 | 24 | func applicationWillResignActive(_ application: UIApplication) { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 27 | } 28 | 29 | func applicationDidEnterBackground(_ application: UIApplication) { 30 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 31 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 32 | } 33 | 34 | func applicationWillEnterForeground(_ application: UIApplication) { 35 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 36 | } 37 | 38 | func applicationDidBecomeActive(_ application: UIApplication) { 39 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 40 | } 41 | 42 | func applicationWillTerminate(_ application: UIApplication) { 43 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 44 | } 45 | 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/20x20.imageset/20x20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/LayoutKitSampleApp/Assets.xcassets/20x20.imageset/20x20.png -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/20x20.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "20x20.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/350x200.imageset/350x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/LayoutKitSampleApp/Assets.xcassets/350x200.imageset/350x200.png -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/350x200.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "350x200.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/50x50.imageset/50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/LayoutKitSampleApp/Assets.xcassets/50x50.imageset/50x50.png -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/50x50.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "50x50.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/nick.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "nick.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/nick.imageset/nick.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/LayoutKitSampleApp/Assets.xcassets/nick.imageset/nick.jpg -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/sergei.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "sergei.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /LayoutKitSampleApp/Assets.xcassets/sergei.imageset/sergei.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/LayoutKitSampleApp/Assets.xcassets/sergei.imageset/sergei.jpg -------------------------------------------------------------------------------- /LayoutKitSampleApp/BackgroundMiniProfileViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import ExampleLayouts 11 | 12 | class BackgroundMiniProfileViewController: UIViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | view.backgroundColor = UIColor.white 18 | edgesForExtendedLayout = UIRectEdge() 19 | 20 | let width = view.bounds.width 21 | DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async { 22 | let nickProfile = MiniProfileLayout( 23 | imageName: "nick.jpg", 24 | name: "Nick Snyder", 25 | headline: "Software Engineer at LinkedIn" 26 | ) 27 | 28 | let arrangement = nickProfile.arrangement(width: width) 29 | DispatchQueue.main.async(execute: { 30 | arrangement.makeViews(in: self.view) 31 | }) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/BatchUpdatesCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /// Example of how batch updates work with a UICollectionView. 13 | class BatchUpdatesCollectionViewController: BatchUpdatesBaseViewController { 14 | private var collectionView: LayoutAdapterCollectionView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | let layout = UICollectionViewFlowLayout() 20 | layout.sectionInset = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0) 21 | layout.minimumLineSpacing = 2 22 | 23 | collectionView = LayoutAdapterCollectionView(frame: view.bounds, collectionViewLayout: layout) 24 | collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 25 | collectionView.backgroundColor = UIColor.white 26 | 27 | view.addSubview(collectionView) 28 | collectionView.layoutAdapter.reload(width: collectionView.bounds.width, synchronous: true, layoutProvider: layoutOne) 29 | 30 | let delay = DispatchTime.now() + 2.0 31 | DispatchQueue.main.asyncAfter(deadline: delay) { 32 | self.collectionView.layoutAdapter.reload( 33 | width: self.collectionView.bounds.width, 34 | synchronous: true, 35 | batchUpdates: self.batchUpdates(), 36 | layoutProvider: self.layoutTwo) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/BatchUpdatesTableViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /// Example of how batch updates work with a UITableView. 13 | class BatchUpdatesTableViewController: BatchUpdatesBaseViewController { 14 | private var tableView: LayoutAdapterTableView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | tableView = LayoutAdapterTableView(frame: view.bounds, style: .grouped) 20 | tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 21 | 22 | view.addSubview(tableView) 23 | tableView.layoutAdapter.reload(width: tableView.bounds.width, synchronous: true, layoutProvider: layoutOne) 24 | 25 | let delay = DispatchTime.now() + 2.0 26 | DispatchQueue.main.asyncAfter(deadline: delay) { 27 | self.tableView.layoutAdapter.reload( 28 | width: self.tableView.bounds.width, 29 | synchronous: true, 30 | batchUpdates: self.batchUpdates(), 31 | layoutProvider: self.layoutTwo) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/Benchmarks/DataBinder.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | protocol DataBinder { 12 | associatedtype DataType 13 | func setData(_ data: DataType) 14 | } 15 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/Benchmarks/FeedItemData.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | /// Data to populate a feed item. 12 | struct FeedItemData { 13 | 14 | let actionText: String 15 | let posterName: String 16 | let posterHeadline: String 17 | let posterTimestamp: String 18 | let posterComment: String 19 | let contentTitle: String 20 | let contentDomain: String 21 | let actorComment: String 22 | 23 | static func generate(count: Int) -> [FeedItemData] { 24 | var datas = [FeedItemData]() 25 | for i in 0.. 0 { 50 | heightConstraint.constant = sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude)).height 51 | } 52 | } 53 | 54 | override func sizeThatFits(_ size: CGSize) -> CGSize { 55 | return layout?.measurement(within: size).size ?? .zero 56 | } 57 | 58 | override var intrinsicContentSize: CGSize { 59 | return sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)) 60 | } 61 | 62 | override func layoutSubviews() { 63 | layout?.measurement(within: bounds.size).arrangement(within: bounds).makeViews(in: self) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/Benchmarks/Stopwatch.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | /// A stopwatch that can record elapsed time and benchmark closures. 12 | class Stopwatch { 13 | 14 | let name: String 15 | private(set) var startTime: CFAbsoluteTime? = nil 16 | private(set) var elapsedTime: CFAbsoluteTime = 0 17 | 18 | init(name: String) { 19 | self.name = name 20 | } 21 | 22 | private func reset() { 23 | elapsedTime = 0 24 | startTime = nil 25 | } 26 | 27 | func resume() { 28 | if startTime == nil { 29 | startTime = CFAbsoluteTimeGetCurrent() 30 | } 31 | } 32 | 33 | func pause() { 34 | if let startTime = startTime { 35 | elapsedTime += CFAbsoluteTimeGetCurrent() - startTime 36 | self.startTime = nil 37 | } 38 | } 39 | 40 | /** 41 | Benchmarks the block and logs the result. 42 | The block is responsible for calling `resume()` and `pause()` on the stopwatch. 43 | */ 44 | static func benchmark(_ name: String, block: @escaping (_ stopwatch: Stopwatch) -> Void) { 45 | autoreleasepool { 46 | let stopwatch = Stopwatch(name: name) 47 | 48 | // Make sure we collect enough samples. 49 | let minimumBenchmarkTime: CFAbsoluteTime = 0.5 50 | let minimumIterationCount = 5 51 | 52 | var iterationCount = 1 53 | while true { 54 | autoreleasepool { 55 | block(stopwatch) 56 | } 57 | if stopwatch.elapsedTime >= minimumBenchmarkTime && iterationCount >= minimumIterationCount { 58 | break 59 | } 60 | iterationCount += 1 61 | } 62 | stopwatch.pause() 63 | 64 | let iterations = NSString(format: "%6d", iterationCount) 65 | let opsPerSecond = NSString(format: "%8.2f", Double(iterationCount)/stopwatch.elapsedTime) 66 | NSLog("Benchmark\t\(opsPerSecond)\tops/s\t\(iterations)\titerations\t\(name)\t") 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/FeedBaseViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | import ExampleLayouts 12 | 13 | /// A base class for various view controllers that display a fake feed. 14 | class FeedBaseViewController: UIViewController { 15 | 16 | private var cachedFeedItems: [Layout]? 17 | 18 | func getFeedItems() -> [Layout] { 19 | if let cachedFeedItems = cachedFeedItems { 20 | return cachedFeedItems 21 | } 22 | 23 | let profileCard = ProfileCardLayout( 24 | name: "Nick Snyder", 25 | connectionDegree: "1st", 26 | headline: "Software Engineer at LinkedIn", 27 | timestamp: "5 minutes ago", 28 | profileImageName: "50x50" 29 | ) 30 | 31 | let content = ContentLayout(title: "Chuck Norris", domain: "chucknorris.com") 32 | 33 | let feedItem = FeedItemLayout( 34 | actionText: "Sergei Tauger commented on this", 35 | posterProfile: profileCard, 36 | posterComment: "Check it out", 37 | contentLayout: content, 38 | actorComment: "Awesome!" 39 | ) 40 | 41 | let feedItems = [Layout](repeating: feedItem, count: 1000) 42 | cachedFeedItems = feedItems 43 | return feedItems 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/FeedCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /// Displays the feed using a UICollectionView. 13 | class FeedCollectionViewController: FeedBaseViewController { 14 | 15 | private var reloadableViewLayoutAdapter: ReloadableViewLayoutAdapter! 16 | private var collectionView: UICollectionView! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewFlowLayout()) 22 | collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 23 | collectionView.backgroundColor = UIColor.purple 24 | 25 | reloadableViewLayoutAdapter = ReloadableViewLayoutAdapter(reloadableView: collectionView) 26 | collectionView.dataSource = reloadableViewLayoutAdapter 27 | collectionView.delegate = reloadableViewLayoutAdapter 28 | 29 | view.addSubview(collectionView) 30 | self.layoutFeed(width: collectionView.frame.width, synchronous: false) 31 | } 32 | 33 | private func layoutFeed(width: CGFloat, synchronous: Bool) { 34 | reloadableViewLayoutAdapter.reload(width: width, synchronous: synchronous, layoutProvider: { [weak self] in 35 | return [Section(header: nil, items: self?.getFeedItems() ?? [], footer: nil)] 36 | }) 37 | } 38 | 39 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 40 | super.viewWillTransition(to: size, with: coordinator) 41 | layoutFeed(width: size.width, synchronous: true) 42 | } 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/FeedScrollViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /// Displays a feed using a UIScrollView 13 | class FeedScrollViewController: FeedBaseViewController { 14 | private var scrollView: UIScrollView! 15 | private var cachedFeedLayout: Layout? 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | view.backgroundColor = UIColor.purple 20 | 21 | scrollView = UIScrollView(frame: view.bounds) 22 | scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 23 | view.addSubview(scrollView) 24 | 25 | self.layoutFeed(width: self.view.bounds.width) 26 | } 27 | 28 | private func layoutFeed(width: CGFloat) { 29 | let _ = CFAbsoluteTimeGetCurrent() 30 | DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async { 31 | let arrangement = self.getFeedLayout().arrangement(width: width) 32 | DispatchQueue.main.async(execute: { 33 | self.scrollView.contentSize = arrangement.frame.size 34 | arrangement.makeViews(in: self.scrollView) 35 | let _ = CFAbsoluteTimeGetCurrent() 36 | // NSLog("user: \((end-start).ms)") 37 | }) 38 | } 39 | } 40 | 41 | func getFeedLayout() -> Layout { 42 | if let cachedFeedLayout = cachedFeedLayout { 43 | return cachedFeedLayout 44 | } 45 | 46 | let feedItems = getFeedItems() 47 | let feedLayout = StackLayout( 48 | axis: .vertical, 49 | distribution: .leading, 50 | sublayouts: feedItems 51 | ) 52 | cachedFeedLayout = feedLayout 53 | return feedLayout 54 | } 55 | 56 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 57 | super.viewWillTransition(to: size, with: coordinator) 58 | layoutFeed(width: size.width) 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/FeedTableViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /// Displays a feed using a UITableView 13 | class FeedTableViewController: FeedBaseViewController { 14 | 15 | private var reloadableViewLayoutAdapter: ReloadableViewLayoutAdapter! 16 | private var tableView: UITableView! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | tableView = UITableView(frame: view.bounds, style: .grouped) 22 | tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 23 | tableView.backgroundColor = UIColor.purple 24 | 25 | reloadableViewLayoutAdapter = ReloadableViewLayoutAdapter(reloadableView: tableView) 26 | tableView.dataSource = reloadableViewLayoutAdapter 27 | tableView.delegate = reloadableViewLayoutAdapter 28 | 29 | view.addSubview(tableView) 30 | self.layoutFeed(width: tableView.frame.width, synchronous: false) 31 | } 32 | 33 | private func layoutFeed(width: CGFloat, synchronous: Bool) { 34 | reloadableViewLayoutAdapter.reload(width: width, synchronous: synchronous, layoutProvider: { [weak self] in 35 | return [Section(header: nil, items: self?.getFeedItems() ?? [], footer: nil)] 36 | }) 37 | } 38 | 39 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 40 | super.viewWillTransition(to: size, with: coordinator) 41 | layoutFeed(width: size.width, synchronous: true) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/ForegroundProfileViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import ExampleLayouts 11 | 12 | class ForegroundMiniProfileViewController: UIViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | view.backgroundColor = UIColor.white 18 | edgesForExtendedLayout = UIRectEdge() 19 | 20 | let nickProfile = MiniProfileLayout( 21 | imageName: "nick.jpg", 22 | name: "Nick Snyder", 23 | headline: "Software Engineer at LinkedIn" 24 | ) 25 | 26 | nickProfile.arrangement(width: view.bounds.width).makeViews(in: view) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | UIInterfaceOrientationPortraitUpsideDown 35 | 36 | UISupportedInterfaceOrientations~ipad 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationPortraitUpsideDown 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/LabledImageLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /** 13 | An image stacked on top of a label. 14 | */ 15 | class LabeledImageLayout: StackLayout { 16 | 17 | init(imageUrl: URL, imageSize: CGSize, labelText: String) { 18 | let image = UrlImageLayout(url: imageUrl, size: imageSize) 19 | let label = LabelLayout(text: labelText, alignment: Alignment(vertical: .top, horizontal: .center)) 20 | super.init( 21 | axis: .vertical, 22 | spacing: 8, 23 | distribution: .leading, 24 | alignment: .fill, 25 | sublayouts: [image, label] 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/MenuViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | /// The main menu for the sample app. 12 | class MenuViewController: UITableViewController { 13 | 14 | private let reuseIdentifier = " " 15 | 16 | private let viewControllers: [UIViewController.Type] = [ 17 | BenchmarkViewController.self, 18 | FeedScrollViewController.self, 19 | FeedCollectionViewController.self, 20 | FeedTableViewController.self, 21 | StackViewController.self, 22 | NestedCollectionViewController.self, 23 | ForegroundMiniProfileViewController.self, 24 | BackgroundMiniProfileViewController.self, 25 | OverlayViewController.self, 26 | BatchUpdatesCollectionViewController.self, 27 | BatchUpdatesTableViewController.self 28 | ] 29 | 30 | convenience init() { 31 | self.init(style: .grouped) 32 | title = "Menu" 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier) 38 | } 39 | 40 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 41 | return viewControllers.count 42 | } 43 | 44 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 45 | let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) 46 | cell.textLabel?.text = String(describing: viewControllers[indexPath.row]) 47 | return cell 48 | } 49 | 50 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 51 | let viewController = viewControllers[indexPath.row].init() 52 | viewController.title = String(describing: viewController) 53 | navigationController?.pushViewController(viewController, animated: true) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/StackViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /** 13 | Uses a stack view to layout subviews. 14 | */ 15 | class StackViewController: UIViewController { 16 | 17 | private var stackView: StackView! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | edgesForExtendedLayout = UIRectEdge() 23 | 24 | stackView = StackView(axis: .vertical, spacing: 4) 25 | stackView.addArrangedSubviews([ 26 | UILabel(text: "Nick"), 27 | UILabel(text: "Software Engineer") 28 | ]) 29 | stackView.frame = view.bounds 30 | stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 31 | stackView.backgroundColor = UIColor.purple 32 | 33 | view.addSubview(stackView) 34 | } 35 | } 36 | 37 | extension UILabel { 38 | convenience init(text: String) { 39 | self.init() 40 | self.text = text 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LayoutKitSampleApp/UrlImageLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | /** 13 | A layout for an image that is loaded by URL. 14 | The size of the layout must be specified before the image is actually loaded. 15 | */ 16 | class UrlImageLayout: SizeLayout { 17 | 18 | init(url: URL, size: CGSize) { 19 | let config = { (imageView: UrlImageView) in 20 | imageView.backgroundColor = UIColor.orange 21 | imageView.url = url 22 | DispatchQueue.global(qos: .background).async(execute: { 23 | guard let data = try? Data(contentsOf: url) else { 24 | NSLog("failed to load image data \(url)") 25 | return 26 | } 27 | DispatchQueue.main.async(execute: { 28 | if imageView.url == url { 29 | imageView.image = UIImage(data: data) 30 | } 31 | }) 32 | }) 33 | } 34 | super.init(minWidth: size.width, 35 | maxWidth: size.width, 36 | minHeight: size.height, 37 | maxHeight: size.height, 38 | alignment: .center, 39 | flexibility: .inflexible, 40 | config: config) 41 | } 42 | } 43 | 44 | /// An UIImageView that has an associated url. 45 | class UrlImageView: UIImageView { 46 | var url: URL? = nil 47 | } 48 | -------------------------------------------------------------------------------- /LayoutKitTests/AlignmentTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import XCTest 10 | import LayoutKit 11 | 12 | class AlignmentTests: XCTestCase { 13 | 14 | func testAspectFit() { 15 | let sdtvDisplayedOnHDTV = Alignment.aspectFit.position(size: CGSize(width: 4, height: 3), in: CGRect(x: 100, y: 200, width: 16, height: 9)) 16 | XCTAssertEqual(sdtvDisplayedOnHDTV, CGRect(x: 102, y: 200, width: 12, height: 9)) 17 | 18 | let hdtvDisplayedOnSDTV = Alignment.aspectFit.position(size: CGSize(width: 16, height: 9), in: CGRect(x: 100, y: 200, width: 4, height: 3)) 19 | let expressionThatIsTooComplicatedForSwiftCompiler: CGFloat = (3 - 4*9/16.0)/2.0 20 | XCTAssertEqual(hdtvDisplayedOnSDTV, CGRect(x: 100, y: 200 + expressionThatIsTooComplicatedForSwiftCompiler, width: 4, height: 4*9/16.0)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LayoutKitTests/CollectionExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | 10 | extension Collection { 11 | 12 | /// Returns the only element in the collection. 13 | /// It returns nil if there is not exactly one element in the collection. 14 | public var only: Self.Iterator.Element? { 15 | if count == 1 { 16 | return first 17 | } 18 | return nil 19 | } 20 | 21 | /// Returns the element at the specified index iff it is within bounds, otherwise nil. 22 | /// http://stackoverflow.com/questions/25329186/safe-bounds-checked-array-lookup-in-swift-through-optional-bindings 23 | public subscript(safe index: Index) -> Element? { 24 | return index >= startIndex && index < endIndex ? self[index] : nil 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LayoutKitTests/CollectionViewTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import XCTest 10 | import LayoutKit 11 | 12 | /** 13 | Experiment with how UICollectionView works. 14 | */ 15 | class CollectionViewTests: XCTestCase, UICollectionViewDataSource { 16 | 17 | var sectionCounts = [Int]() 18 | 19 | let reuseIdentifier = "reuseIdentifier" 20 | 21 | func testCollectionView() { 22 | let layout = UICollectionViewFlowLayout() 23 | let view = UICollectionView(frame: CGRect(x: 0, y: 0, width: 320, height: 480), collectionViewLayout: layout) 24 | view.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier) 25 | view.dataSource = self 26 | 27 | sectionCounts = [1] 28 | log("reload start") 29 | 30 | view.reloadData() 31 | layout.prepare() 32 | log("reload end") 33 | 34 | let e = expectation(description: "main") 35 | DispatchQueue.main.async { 36 | log("insert start") 37 | self.sectionCounts = [2] 38 | view.insertItems(at: [IndexPath(item: 1, section: 0)]) 39 | log("insert end") 40 | 41 | DispatchQueue.main.async(execute: { 42 | log("insert start") 43 | self.sectionCounts = [2, 1] 44 | view.insertSections(IndexSet(integer: 1)) 45 | log("insert end") 46 | 47 | e.fulfill() 48 | }) 49 | } 50 | 51 | waitForExpectations(timeout: 10, handler: nil) 52 | } 53 | 54 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 55 | log("numberOfItemsInSection \(section) = \(sectionCounts[section])") 56 | return sectionCounts[section] 57 | } 58 | 59 | func numberOfSections(in collectionView: UICollectionView) -> Int { 60 | log("numberOfSections = \(sectionCounts.count)") 61 | return sectionCounts.count 62 | } 63 | 64 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 65 | log("cellForItemAtIndexPath \(indexPath)") 66 | return collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) 67 | } 68 | } 69 | 70 | private func log(_ msg: String) { 71 | //NSLog("%@", msg) 72 | } 73 | -------------------------------------------------------------------------------- /LayoutKitTests/DensityAssertions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | import XCTest 11 | import LayoutKit 12 | 13 | let oneThird = 1.0 / 3.0 14 | let twoThirds = 2.0 / 3.0 15 | 16 | private let densityAccuracy: CGFloat = 0.00001 17 | 18 | func AssertEqualDensity(_ actual: CGRect, _ expected: [CGFloat: CGRect], file: StaticString = #file, line: UInt = #line) { 19 | guard let expected = expectationForCurrentDensity(expected, file: file, line: line) else { 20 | return 21 | } 22 | XCTAssertEqual(actual.origin.x, expected.origin.x, accuracy: densityAccuracy, file: file, line: line) 23 | XCTAssertEqual(actual.origin.y, expected.origin.y, accuracy: densityAccuracy, file: file, line: line) 24 | XCTAssertEqual(actual.size.width, expected.size.width, accuracy: densityAccuracy, file: file, line: line) 25 | XCTAssertEqual(actual.size.height, expected.size.height, accuracy: densityAccuracy, file: file, line: line) 26 | } 27 | 28 | func AssertEqualDensity(_ actual: CGSize, _ expected: [CGFloat: CGSize], file: StaticString = #file, line: UInt = #line) { 29 | guard let expected = expectationForCurrentDensity(expected, file: file, line: line) else { 30 | return 31 | } 32 | XCTAssertEqual(actual.width, expected.width, accuracy: densityAccuracy, file: file, line: line) 33 | XCTAssertEqual(actual.height, expected.height, accuracy: densityAccuracy, file: file, line: line) 34 | } 35 | 36 | func AssertEqualDensity(_ actual: CGFloat, _ expected: [CGFloat: CGFloat], file: StaticString = #file, line: UInt = #line) { 37 | guard let expected = expectationForCurrentDensity(expected, file: file, line: line) else { 38 | return 39 | } 40 | XCTAssertEqual(actual, expected, accuracy: densityAccuracy, file: file, line: line) 41 | } 42 | 43 | /// Returns the expectation for the current density. 44 | private func expectationForCurrentDensity(_ expected: [CGFloat: T], file: StaticString, line: UInt) -> T? { 45 | #if os(iOS) 46 | let scale = UIScreen.main.scale 47 | #elseif os(OSX) 48 | let scale = NSScreen.main?.backingScaleFactor ?? 2.0 49 | #elseif os(tvOS) 50 | let scale: CGFloat = 1.0 51 | #endif 52 | 53 | guard let expected = expected[scale] else { 54 | XCTFail("test does not have an expectation for screen scale \(scale)", file: file, line: line) 55 | return nil 56 | } 57 | return expected 58 | } 59 | -------------------------------------------------------------------------------- /LayoutKitTests/IndexSetExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | extension IndexSet { 12 | /// Returns the only index in the set. 13 | /// It returns nil if there is not exactly one index in the set. 14 | public var only: Int? { 15 | if count == 1 { 16 | return first 17 | } 18 | return nil 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LayoutKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LayoutKitTests/InsetLayoutTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import XCTest 10 | import LayoutKit 11 | 12 | class InsetLayoutTests: XCTestCase { 13 | 14 | #if os(iOS) 15 | func testInsetLabel() { 16 | let insetLabel = InsetLayout( 17 | insets: EdgeInsets(top: 2, left: 4, bottom: 8, right: 16), 18 | sublayout: LabelLayout(text: "Hi", font: UIFont.helvetica()) 19 | ) 20 | let arrangement = insetLabel.arrangement() 21 | AssertEqualDensity(arrangement.frame, [ 22 | 2.0: CGRect(x: 0, y: 0, width: 4+16.5+16, height: 2+20+8), 23 | 3.0: CGRect(x: 0, y: 0, width: CGFloat(4+16+oneThird+16), height: CGFloat(2+20-oneThird+8)), 24 | ]) 25 | AssertEqualDensity(arrangement.sublayouts.first!.frame, [ 26 | 2.0: CGRect(x: 4, y: 2, width: 16.5, height: 20), 27 | 3.0: CGRect(x: 4, y: 2, width: 16+oneThird, height: 20-oneThird), 28 | ]) 29 | } 30 | #endif 31 | 32 | func testInsetConvenience() { 33 | let insetLayout = InsetLayout( 34 | inset: 2, 35 | sublayout: SizeLayout(width: 10, height: 10) 36 | ) 37 | 38 | let arrangement = insetLayout.arrangement() 39 | XCTAssertEqual(arrangement.frame, CGRect(x: 0, y: 0, width: 14, height: 14)) 40 | XCTAssertEqual(arrangement.sublayouts.first!.frame, CGRect(x: 2, y: 2, width: 10, height: 10)) 41 | } 42 | 43 | func testConfig() { 44 | var configCount = 0 45 | let insetLayout = InsetLayout( 46 | insets: EdgeInsets(top: 2, left: 4, bottom: 8, right: 16), 47 | sublayout: SizeLayout(width: 10, height: 10), 48 | config: { view in 49 | configCount += 1 50 | } 51 | ) 52 | let insetView = insetLayout.arrangement().makeViews() 53 | XCTAssertNotNil(insetView) 54 | XCTAssertEqual(configCount, 1) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LayoutKitTests/ReloadableViewLayoutAdapterCollectionViewOverrideTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import XCTest 10 | @testable import LayoutKit 11 | 12 | class ReloadableViewLayoutAdapterCollectionViewOverrideTests: XCTestCase { 13 | 14 | var reloadableViewLayoutAdapter: MockReloadableCollectionViewAdapter! 15 | var collectionView: UICollectionView! 16 | 17 | override func setUp() { 18 | collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout()) 19 | reloadableViewLayoutAdapter = MockReloadableCollectionViewAdapter(reloadableView: collectionView) 20 | } 21 | 22 | func testSizeForItemAt_Called_ShouldCallMock() { 23 | _ = reloadableViewLayoutAdapter.collectionView(collectionView, layout: UICollectionViewFlowLayout(), sizeForItemAt: IndexPath(item: 0, section: 0)) 24 | 25 | XCTAssert(reloadableViewLayoutAdapter.collectionViewSizeForItemAtCallCount == 1) 26 | } 27 | 28 | 29 | func testCellForItemAt_Called_ShouldCallMock() { 30 | _ = reloadableViewLayoutAdapter.collectionView(collectionView, cellForItemAt: IndexPath(row: 0, section: 0)) 31 | 32 | XCTAssert(reloadableViewLayoutAdapter.collectionViewCellForItemAtCallCount == 1) 33 | } 34 | 35 | func testViewForSupplementaryElementOfKind_Called_ShouldCallMock() { 36 | _ = reloadableViewLayoutAdapter.collectionView(collectionView, viewForSupplementaryElementOfKind: "", at: IndexPath(row: 0, section: 0)) 37 | 38 | XCTAssert(reloadableViewLayoutAdapter.collectionViewViewForSupplementaryElementOfKindCallCount == 1) 39 | } 40 | } 41 | 42 | class MockReloadableCollectionViewAdapter: ReloadableViewLayoutAdapter { 43 | 44 | var collectionViewSizeForItemAtCallCount = 0 45 | 46 | override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 47 | collectionViewSizeForItemAtCallCount += 1 48 | 49 | return CGSize.zero 50 | } 51 | 52 | var collectionViewCellForItemAtCallCount = 0 53 | 54 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 55 | collectionViewCellForItemAtCallCount += 1 56 | 57 | return UICollectionViewCell() 58 | } 59 | 60 | var collectionViewViewForSupplementaryElementOfKindCallCount = 0 61 | 62 | override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 63 | collectionViewViewForSupplementaryElementOfKindCallCount += 1 64 | 65 | return UICollectionReusableView() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /LayoutKitTests/ReloadableViewLayoutAdapterTableViewOverrideTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import XCTest 10 | @testable import LayoutKit 11 | 12 | class ReloadableViewLayoutAdapterTableViewOverrideTests: XCTestCase { 13 | 14 | var reloadableViewLayoutAdapter: MockReloadableTableViewAdapter! 15 | var tableView: UITableView! 16 | 17 | override func setUp() { 18 | tableView = UITableView() 19 | reloadableViewLayoutAdapter = MockReloadableTableViewAdapter(reloadableView: tableView) 20 | } 21 | 22 | func testCellForRowAt_Called_ShouldCallMock() { 23 | _ = reloadableViewLayoutAdapter.tableView(tableView, cellForRowAt: IndexPath(item: 0, section: 0)) 24 | 25 | XCTAssert(reloadableViewLayoutAdapter.tableViewCellForRowAtCallCount == 1) 26 | } 27 | 28 | 29 | func testNumberOfRowsInSection_Called_ShouldCallMock() { 30 | _ = reloadableViewLayoutAdapter.tableView(tableView, numberOfRowsInSection: 0) 31 | 32 | XCTAssert(reloadableViewLayoutAdapter.tableViewNumberOfRowsInSectionCallCount == 1) 33 | } 34 | 35 | func testHeightForFooterInSection_Called_ShouldCallMock() { 36 | _ = reloadableViewLayoutAdapter.tableView(tableView, heightForFooterInSection: 0) 37 | 38 | XCTAssert(reloadableViewLayoutAdapter.tableViewHeightForFooterInSectionCallCount == 1) 39 | } 40 | } 41 | 42 | class MockReloadableTableViewAdapter: ReloadableViewLayoutAdapter { 43 | 44 | var tableViewHeightForFooterInSectionCallCount = 0 45 | 46 | override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 47 | tableViewHeightForFooterInSectionCallCount += 1 48 | 49 | return 0 50 | } 51 | 52 | var tableViewNumberOfRowsInSectionCallCount = 0 53 | 54 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 55 | tableViewNumberOfRowsInSectionCallCount += 1 56 | 57 | return 0 58 | } 59 | 60 | var tableViewCellForRowAtCallCount = 0 61 | 62 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 63 | tableViewCellForRowAtCallCount += 1 64 | 65 | return UITableViewCell() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /LayoutKitTests/StackLayoutFlexibilityTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import XCTest 10 | import LayoutKit 11 | 12 | class StackLayoutFlexibilityTests: XCTestCase { 13 | 14 | func testVerticalFlexibility() { 15 | let stack = StackLayout( 16 | axis: .vertical, 17 | sublayouts: [ 18 | SizeLayout(width: 1, height: 1, flexibility: .low), 19 | SizeLayout(width: 1, height: 1, flexibility: .flexible), 20 | SizeLayout(width: 1, height: 1, flexibility: .high), 21 | ] 22 | ) 23 | XCTAssertEqual(stack.flexibility.horizontal, Flexibility.lowFlex) 24 | XCTAssertEqual(stack.flexibility.vertical, Flexibility.highFlex) 25 | } 26 | 27 | func testVerticalInflexibility() { 28 | let stack = StackLayout( 29 | axis: .vertical, 30 | sublayouts: [ 31 | SizeLayout(width: 1, height: 1, flexibility: .low), 32 | SizeLayout(width: 1, height: 1, flexibility: .inflexible), 33 | SizeLayout(width: 1, height: 1, flexibility: .high), 34 | ] 35 | ) 36 | XCTAssertEqual(stack.flexibility.horizontal, nil) 37 | XCTAssertEqual(stack.flexibility.vertical, Flexibility.highFlex) 38 | } 39 | 40 | func testHorizontalFlexibility() { 41 | let stack = StackLayout( 42 | axis: .horizontal, 43 | sublayouts: [ 44 | SizeLayout(width: 1, height: 1, flexibility: .low), 45 | SizeLayout(width: 1, height: 1, flexibility: .flexible), 46 | SizeLayout(width: 1, height: 1, flexibility: .high), 47 | ] 48 | ) 49 | XCTAssertEqual(stack.flexibility.horizontal, Flexibility.highFlex) 50 | XCTAssertEqual(stack.flexibility.vertical, Flexibility.lowFlex) 51 | } 52 | 53 | func testHorizontalInflexibility() { 54 | let stack = StackLayout( 55 | axis: .horizontal, 56 | sublayouts: [ 57 | SizeLayout(width: 1, height: 1, flexibility: .low), 58 | SizeLayout(width: 1, height: 1, flexibility: .inflexible), 59 | SizeLayout(width: 1, height: 1, flexibility: .high), 60 | ] 61 | ) 62 | XCTAssertEqual(stack.flexibility.horizontal, Flexibility.highFlex) 63 | XCTAssertEqual(stack.flexibility.vertical, nil) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /LayoutKitTests/StackLayoutTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import XCTest 10 | import LayoutKit 11 | 12 | class StackLayoutTests: XCTestCase { 13 | 14 | #if os(iOS) 15 | func testTwoLabelVerticalStack() { 16 | let stack = StackLayout( 17 | axis: .vertical, 18 | spacing: 4, 19 | distribution: .leading, 20 | sublayouts: [ 21 | LabelLayout(text: "Hi", font: UIFont.helvetica()), 22 | LabelLayout(text: "Nick Snyder", font: UIFont.helvetica()), 23 | ] 24 | ) 25 | let arrangement = stack.arrangement() 26 | 27 | AssertEqualDensity(arrangement.frame, [ 28 | 2.0: CGRect(x: 0, y: 0, width: 92, height: 44), 29 | 3.0: CGRect(x: 0, y: 0, width: 91 + twoThirds, height: 43 + oneThird) 30 | ]) 31 | } 32 | #endif 33 | 34 | func testConfig() { 35 | var configCount = 0 36 | let stack = StackLayout( 37 | axis: .vertical, 38 | sublayouts: [ 39 | SizeLayout(width: 1, height: 1) 40 | ], 41 | config: { view in 42 | configCount += 1 43 | } 44 | ) 45 | let stackView = stack.arrangement().makeViews() 46 | XCTAssertNotNil(stackView) 47 | XCTAssertEqual(configCount, 1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LayoutKitTests/TableViewTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import XCTest 10 | import LayoutKit 11 | 12 | /** 13 | Experiment with how UITableView works. 14 | */ 15 | class TableViewTests: XCTestCase, UITableViewDataSource, UITableViewDelegate { 16 | 17 | var sectionCount = 0 18 | var dataCount = 1 19 | 20 | let reuseIdentifier = "reuseIdentifier" 21 | 22 | func testTableView() { 23 | let view = UITableView(frame: CGRect(x: 0, y: 0, width: 320, height: 480), style: .grouped) 24 | view.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier) 25 | view.dataSource = self 26 | view.delegate = self 27 | 28 | sectionCount = 1 29 | log("reload start") 30 | view.reloadData() 31 | log("reload end") 32 | 33 | log("insert start") 34 | dataCount += 1 35 | let indexPaths = [IndexPath(item: 1, section: 0)] 36 | view.insertRows(at: indexPaths, with: .none) 37 | log("insert end") 38 | } 39 | 40 | func numberOfSections(in tableView: UITableView) -> Int { 41 | log("numberOfSections = \(sectionCount)") 42 | return sectionCount 43 | } 44 | 45 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 46 | log("numberOfRowsInSection \(section) = \(dataCount)") 47 | return dataCount 48 | } 49 | 50 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 51 | return tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) 52 | } 53 | } 54 | 55 | private func log(_ msg: String) { 56 | //NSLog("%@", msg) 57 | } 58 | -------------------------------------------------------------------------------- /LayoutKitTests/TestStack.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | import LayoutKit 11 | 12 | class TestStack { 13 | 14 | let intrinsicSize: CGSize 15 | 16 | var stackLayout: StackLayout! = nil 17 | var stackView: View! = nil 18 | var oneView: View! = nil 19 | var twoView: View! = nil 20 | var threeView: View! = nil 21 | 22 | init(axis: Axis, distribution: StackLayoutDistribution, spacing: CGFloat = 0, alignment: Alignment = .fill) { 23 | 24 | switch axis { 25 | case .vertical: 26 | self.intrinsicSize = CGSize(width: 33.5, height: CGFloat(2*spacing + 14 + 18.5 + 23)) 27 | case .horizontal: 28 | self.intrinsicSize = CGSize(width: CGFloat(2*spacing + 7 + 18 + 33.5), height: 23) 29 | } 30 | 31 | stackLayout = StackLayout( 32 | axis: axis, 33 | spacing: spacing, 34 | distribution: distribution, 35 | alignment: alignment, 36 | sublayouts: [ 37 | SizeLayout(width: 7, height: 14, alignment: .fill, flexibility: .flexible, config: { view in 38 | self.oneView = view 39 | }), 40 | SizeLayout(width: 18, height: 18.5, alignment: .fill, flexibility: .flexible, config: { view in 41 | self.twoView = view 42 | }), 43 | SizeLayout(width: 33.5, height: 23, alignment: .fill, flexibility: .flexible, config: { view in 44 | self.threeView = view 45 | }), 46 | ], 47 | config: { view in 48 | self.stackView = view 49 | } 50 | ) 51 | } 52 | 53 | func arrangement(excessWidth: CGFloat? = nil, excessHeight: CGFloat? = nil) -> TestStack { 54 | // width/height default to nil to match what real callers would do. 55 | // defaulting to 0 would be less code but real callers are unlikely to explicitly provide both width and height. 56 | var width: CGFloat? = nil 57 | if let excessWidth = excessWidth { 58 | width = intrinsicSize.width + excessWidth 59 | } 60 | var height: CGFloat? = nil 61 | if let excessHeight = excessHeight { 62 | height = intrinsicSize.height + excessHeight 63 | } 64 | stackLayout.arrangement(width: width, height: height).makeViews() 65 | return self 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /LayoutKitTests/TextExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | extension Text { 13 | struct TestCase { 14 | let text: Text 15 | let font: UIFont? 16 | } 17 | 18 | static var testCases: [TestCase] { 19 | let fontNames: [String?] = [ 20 | nil, 21 | "Helvetica", 22 | "Helvetica Neue" 23 | ] 24 | 25 | let texts: [Text] = [ 26 | .unattributed(""), 27 | .unattributed(" "), 28 | .unattributed("Hi"), 29 | .unattributed("Hello world"), 30 | .unattributed("Hello! 😄😄😄"), 31 | .attributed(NSAttributedString(string: "")), 32 | .attributed(NSAttributedString(string: " ")), 33 | .attributed(NSAttributedString(string: "", attributes: [NSAttributedString.Key.font: UIFont.helvetica(size: 42)])), 34 | .attributed(NSAttributedString(string: " ", attributes: [NSAttributedString.Key.font: UIFont.helvetica(size: 42)])), 35 | .attributed(NSAttributedString(string: "Hi")), 36 | .attributed(NSAttributedString(string: "Hello world")), 37 | .attributed(NSAttributedString(string: "Hello! 😄😄😄")), 38 | .attributed(NSAttributedString(string: "Hello! 😄😄😄", attributes: [NSAttributedString.Key.font: UIFont.helvetica(size: 42)])), 39 | ] 40 | 41 | let fontSizes = 0...20 42 | 43 | var tests = [TestCase]() 44 | for fontName in fontNames { 45 | for fontSize in fontSizes { 46 | let font = fontName.flatMap({ (fontName) -> UIFont? in 47 | return UIFont(name: fontName, size: CGFloat(fontSize)) 48 | }) 49 | for text in texts { 50 | tests.append(TestCase(text: text, font: font)) 51 | } 52 | } 53 | 54 | } 55 | return tests 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LayoutKitTests/UIFontExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | extension UIFont { 12 | 13 | /** 14 | Returns helvetica font. 15 | Layout tests should always explicitly specify a font so that sizes don't change if the default system font or font size changes. 16 | */ 17 | static func helvetica(size: CGFloat = 17) -> UIFont { 18 | return UIFont(name: "Helvetica", size: size)! 19 | } 20 | 21 | static func helveticaNeue(size: CGFloat = 17) -> UIFont { 22 | return UIFont(name: "Helvetica Neue", size: size)! 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2016 LinkedIn Corp. 2 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, 6 | software distributed under the License is distributed on an "AS IS" BASIS, 7 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | // swift-tools-version:3.1 10 | 11 | import PackageDescription 12 | 13 | let package = Package( 14 | name: "LayoutKit" 15 | ) 16 | -------------------------------------------------------------------------------- /RELEASE-CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # Release checklist 2 | 3 | How to release a new version of LayoutKit. 4 | 5 | - Verify that all tests are passing. 6 | 7 | [![Build Status](https://travis-ci.org/linkedin/LayoutKit.svg?branch=master)](https://travis-ci.org/linkedin/LayoutKit) 8 | 9 | - Bump version in `LayoutKit.podspec` and `LayoutKitObjC.podspec`. 10 | 11 | `spec.version = '2.0.0'` 12 | 13 | - Bump the version in `Sources/Info.plist` 14 | 15 | ``` 16 | CFBundleShortVersionString 17 | 2.0.0 18 | ``` 19 | 20 | - Verify that the podspec is valid. 21 | 22 | ``` 23 | $ pod lib lint 24 | 25 | -> LayoutKit (2.0.0) 26 | 27 | LayoutKit passed validation. 28 | ``` 29 | 30 | - Push version bumps to GitHub 31 | - [Draft a release](https://github.com/linkedin/LayoutKit/releases) in Github and write release notes. 32 | - Deploy the documentation 33 | 34 | ```bash 35 | $ mkdocs gh-deploy --clean 36 | INFO - Cleaning site directory 37 | INFO - Building documentation to directory: /Users/nsnyder/code/linkedin/layoutkit-opensource/site 38 | INFO - Copying '/Users/nsnyder/code/linkedin/layoutkit-opensource/site' to 'gh-pages' branch and pushing to GitHub. 39 | INFO - Based on your CNAME file, your documentation should be available shortly at: http://layoutkit.org 40 | INFO - NOTE: Your DNS records must be configured appropriately for your CNAME URL to work. 41 | ``` 42 | 43 | - Push the podspec to Cocoapods master repo. 44 | 45 | `$ pod trunk push` 46 | 47 | - 👍 48 | 49 | # Required release tools 50 | 51 | - cocoapods (try beta if cocoapods have any issues) 52 | 53 | `$ sudo gem install cocoapods` 54 | 55 | - mkdocs 56 | 57 | `$ brew install mkdocs` 58 | 59 | -------------------------------------------------------------------------------- /Sources/Animation.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | 10 | /// An animation for a layout. 11 | public struct Animation { 12 | 13 | let arrangement: LayoutArrangement 14 | let rootView: View 15 | let direction: UserInterfaceLayoutDirection 16 | 17 | /// Apply the final state of the animation. 18 | /// Call this inside a UIKit animation block. 19 | public func apply() { 20 | arrangement.makeViews(in: rootView, direction: direction) 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Sources/AppKitSupport.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import AppKit 10 | 11 | public typealias View = NSView 12 | 13 | public typealias UserInterfaceLayoutDirection = NSUserInterfaceLayoutDirection 14 | 15 | extension NSView { 16 | 17 | func convertToAbsoluteCoordinates(_ rect: CGRect) -> CGRect { 18 | return convert(rect, to: nil) 19 | } 20 | 21 | func convertFromAbsoluteCoordinates(_ rect: CGRect) -> CGRect { 22 | return convert(rect, from: nil) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Axis.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | public enum Axis { 10 | 11 | /// The y-axis. 12 | case vertical 13 | 14 | /// The x-axis. 15 | case horizontal 16 | } 17 | -------------------------------------------------------------------------------- /Sources/ConfigurableLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | /** 12 | Convenient optional protocol for layout implementations to use instead of `Layout`. 13 | 14 | It requires a more typesafe `configure(view:)` method that is used to implement 15 | `configure(baseViewType:)` in the Layout protocol. 16 | */ 17 | public protocol ConfigurableLayout: Layout { 18 | 19 | /** 20 | The class of view that should be created for this layout, if it needs a view. 21 | This is specified by the conforming class via its implementation of `configure(view:)`. 22 | */ 23 | associatedtype ConfigurableView: View 24 | 25 | /** 26 | Configures the given view. 27 | 28 | When implementing this method, use the specific concrete type for ConfigurableView. 29 | 30 | Example: 31 | 32 | class LabelLayout { 33 | func configure(view label: UILabel) { 34 | label.text = "example" 35 | } 36 | } 37 | 38 | MUST be run on the main thread. 39 | */ 40 | func configure(view: ConfigurableView) 41 | } 42 | 43 | // Implement `configure(baseViewType:)` from `Layout`. 44 | public extension ConfigurableLayout { 45 | func configure(baseTypeView: View) { 46 | guard let view = baseTypeView as? ConfigurableView else { 47 | assertionFailure("Expected baseTypeView \(baseTypeView) to be of type \(ConfigurableView.self) but it was of type \(type(of: baseTypeView))") 48 | return 49 | } 50 | configure(view: view) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 10.1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Internal/CFAbsoluteTimeExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | extension CFAbsoluteTime { 12 | var ms: String { 13 | return String(format: "%4.1f ms", self * 1000) 14 | } 15 | } -------------------------------------------------------------------------------- /Sources/Internal/CGFloatExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | extension CGFloat { 12 | 13 | /** 14 | Returns the current float rounded up to the nearest fraction of a pixel 15 | that the screen density supports. 16 | */ 17 | var roundedUpToFractionalPoint: CGFloat { 18 | if self == 0 { 19 | return 0 20 | } 21 | if self < 0 { 22 | return -(-self).roundedDownToFractionalPoint 23 | } 24 | let scale = UIScreen.main.scale 25 | // The smallest precision in points (aka the number of points per hardware pixel). 26 | let pointPrecision = 1.0 / scale 27 | if self <= pointPrecision { 28 | return pointPrecision 29 | } 30 | return ceil(self * scale) / scale 31 | } 32 | 33 | /** 34 | Returns the current float rounded down to the nearest fraction of a pixel 35 | that the screen density supports. 36 | */ 37 | var roundedDownToFractionalPoint: CGFloat { 38 | if self == 0 { 39 | return 0 40 | } 41 | if self < 0 { 42 | return -(-self).roundedUpToFractionalPoint 43 | } 44 | let scale = UIScreen.main.scale 45 | // The smallest precision in points (aka the number of points per hardware pixel). 46 | let pointPrecision = 1.0 / scale 47 | if self < pointPrecision { 48 | return 0 49 | } 50 | return floor(self * scale) / scale 51 | } 52 | 53 | /** 54 | Returns the current float rounded up or down to the nearest fraction of a pixel 55 | that the screen density supports. 56 | */ 57 | var roundedToFractionalPoint: CGFloat { 58 | if self == 0 { 59 | return 0 60 | } 61 | let up = roundedUpToFractionalPoint 62 | let down = roundedDownToFractionalPoint 63 | return up - self <= self - down ? up : down 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Internal/CGSizeExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | import Foundation 11 | 12 | #if os(OSX) 13 | public typealias EdgeInsets = NSEdgeInsets 14 | #endif 15 | 16 | extension CGSize { 17 | func increased(by insets: EdgeInsets) -> CGSize { 18 | return CGSize( 19 | width: width + insets.left + insets.right, 20 | height: height + insets.top + insets.bottom) 21 | } 22 | 23 | func decreased(by insets: EdgeInsets) -> CGSize { 24 | return CGSize( 25 | width: width - insets.left - insets.right, 26 | height: height - insets.top - insets.bottom) 27 | } 28 | 29 | func decreasedToSize(_ maxSize: CGSize) -> CGSize { 30 | let width = min(self.width, maxSize.width) 31 | let height = min(self.height, maxSize.height) 32 | return CGSize(width: width, height: height) 33 | } 34 | 35 | func increasedToSize(_ minSize: CGSize) -> CGSize { 36 | let width = max(self.width, minSize.width) 37 | let height = max(self.height, minSize.height) 38 | return CGSize(width: width, height: height) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Internal/NSAttributedStringExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | extension NSAttributedString { 12 | 13 | /// Returns a new NSAttributedString with a given font and the same attributes. 14 | func with(font: UIFont) -> NSAttributedString { 15 | return with(additionalAttributes: [NSAttributedString.Key.font: font]) 16 | } 17 | 18 | /// Returns a new NSAttributedString with previous as well as additional attributes. 19 | func with(additionalAttributes: [NSAttributedString.Key : Any]?) -> NSAttributedString { 20 | let attributedTextWithAdditionalAttributes = NSMutableAttributedString(string: string, attributes: additionalAttributes) 21 | let fullRange = NSMakeRange(0, (string as NSString).length) 22 | attributedTextWithAdditionalAttributes.beginEditing() 23 | self.enumerateAttributes(in: fullRange, options: .longestEffectiveRangeNotRequired, using: { (attributes, range, _) in 24 | attributedTextWithAdditionalAttributes.addAttributes(attributes, range: range) 25 | }) 26 | attributedTextWithAdditionalAttributes.endEditing() 27 | return attributedTextWithAdditionalAttributes 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Internal/TextViewDefaultFont.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | /// This class provides default UITextView font 12 | enum TextViewDefaultFont { 13 | 14 | // The font used by UITextView for unattributed strings 15 | static let unattributedTextFont: UIFont = { 16 | #if os(tvOS) 17 | return UIFont.systemFont(ofSize: 38, weight: UIFont.Weight.medium) 18 | #else 19 | return helveticaFont(ofSize: 12) 20 | #endif 21 | }() 22 | 23 | // The font used by UITextView for attributed strings 24 | static let attributedTextFont = helveticaFont(ofSize: 12) 25 | 26 | // The font used by UITextView for empty attributed strings 27 | static let attributedTextFontWithEmptyString: UIFont = { 28 | #if os(tvOS) 29 | return UIFont.systemFont(ofSize: 38, weight: UIFont.Weight.medium) 30 | #else 31 | return helveticaFont(ofSize: 12) 32 | #endif 33 | }() 34 | 35 | private static func helveticaFont(ofSize size: CGFloat) -> UIFont { 36 | guard let font = UIFont(name: "Helvetica", size: size) else { 37 | assertionFailure("`Helvetica` font couldn't be found") 38 | return UIFont.systemFont(ofSize: size) 39 | } 40 | return font 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/LayoutKit.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | #import 10 | 11 | //! Project version number for LayoutKit. 12 | FOUNDATION_EXPORT double LayoutKitVersionNumber; 13 | 14 | //! Project version string for LayoutKit. 15 | FOUNDATION_EXPORT const unsigned char LayoutKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/LayoutKitObjC.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | #import 10 | 11 | //! Project version number for LayoutKitObjC. 12 | FOUNDATION_EXPORT double LayoutKitObjCVersionNumber; 13 | 14 | //! Project version string for LayoutKitObjC. 15 | FOUNDATION_EXPORT const unsigned char LayoutKitObjCVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | #if __has_include("LOKButtonLayoutBuilder.h") 20 | #import "LOKButtonLayoutBuilder.h" 21 | #endif 22 | #import "LOKInsetLayoutBuilder.h" 23 | #if __has_include("LOKLabelLayoutBuilder.h") 24 | #import "LOKLabelLayoutBuilder.h" 25 | #endif 26 | #import "LOKOverlayLayoutBuilder.h" 27 | #import "LOKSizeLayoutBuilder.h" 28 | #import "LOKStackLayoutBuilder.h" 29 | #if __has_include("LOKTextViewLayoutBuilder.h") 30 | #import "LOKTextViewLayoutBuilder.h" 31 | #endif 32 | 33 | #if __has_include("LayoutKitObjC-Swift.h") 34 | #import "LayoutKitObjC-Swift.h" 35 | #endif 36 | -------------------------------------------------------------------------------- /Sources/LayoutMeasurement.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | 11 | /** 12 | The size of a layout and the sizes of its sublayouts. 13 | */ 14 | public struct LayoutMeasurement { 15 | 16 | /// The layout that was measured. 17 | public let layout: Layout 18 | 19 | /// The minimum size of the layout given the maximum size constraint. 20 | public let size: CGSize 21 | 22 | /// The maximum size constraint used during measurement. 23 | public let maxSize: CGSize 24 | 25 | /// The measurements of the layout's sublayouts. 26 | public let sublayouts: [LayoutMeasurement] 27 | 28 | public init(layout: Layout, size: CGSize, maxSize: CGSize, sublayouts: [LayoutMeasurement]) { 29 | self.layout = layout 30 | self.size = size 31 | self.maxSize = maxSize 32 | self.sublayouts = sublayouts 33 | } 34 | 35 | /// Convenience method to position this measured layout. 36 | public func arrangement(within rect: CGRect) -> LayoutArrangement { 37 | return layout.arrangement(within: rect, measurement: self) 38 | } 39 | } -------------------------------------------------------------------------------- /Sources/Layouts/BaseLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | 10 | /** 11 | A base class for layouts. 12 | This layout does not require a view at runtime unless a configuration block has been provided. 13 | */ 14 | open class BaseLayout { 15 | 16 | /// The layout's alignment inside of the rect that it is assigned during arrangement. 17 | public let alignment: Alignment 18 | 19 | /// The flexibility of the layout along both dimensions. 20 | public let flexibility: Flexibility 21 | 22 | /// An identifier for the layout. 23 | /// It is used to identify which views should be reused when animating from one layout to another. 24 | public let viewReuseId: String? 25 | 26 | /// A configuration block that is run on the main thread after the view is created. 27 | public let config: ((V) -> Void)? 28 | 29 | open var needsView: Bool { 30 | return config != nil 31 | } 32 | 33 | private let viewClass: View.Type 34 | 35 | public init(alignment: Alignment, flexibility: Flexibility, viewReuseId: String? = nil, config: ((V) -> Void)?) { 36 | self.alignment = alignment 37 | self.flexibility = flexibility 38 | self.viewReuseId = viewReuseId 39 | self.viewClass = V.self 40 | self.config = config 41 | } 42 | 43 | init(alignment: Alignment, flexibility: Flexibility, viewReuseId: String? = nil, viewClass: V.Type, config: ((V) -> Void)?) { 44 | self.alignment = alignment 45 | self.flexibility = flexibility 46 | self.viewReuseId = viewReuseId 47 | self.viewClass = viewClass 48 | self.config = config 49 | } 50 | 51 | open func configure(view: V) { 52 | config?(view) 53 | } 54 | 55 | open func makeView() -> View { 56 | return viewClass.init() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Math/AxisFlexibility.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | 10 | /// A wrapper around Flexibility that makes it easy to do math relative to an axis. 11 | public struct AxisFlexibility { 12 | public let axis: Axis 13 | public let flexibility: Flexibility 14 | 15 | public var axisFlex: Flexibility.Flex { 16 | get { 17 | switch axis { 18 | case .horizontal: 19 | return flexibility.horizontal 20 | case .vertical: 21 | return flexibility.vertical 22 | } 23 | } 24 | } 25 | 26 | public var crossFlex: Flexibility.Flex { 27 | get { 28 | switch axis { 29 | case .horizontal: 30 | return flexibility.vertical 31 | case .vertical: 32 | return flexibility.horizontal 33 | } 34 | } 35 | } 36 | 37 | public init(axis: Axis, flexibility: Flexibility) { 38 | self.axis = axis 39 | self.flexibility = flexibility 40 | } 41 | 42 | public init(axis: Axis, axisFlex: Flexibility.Flex, crossFlex: Flexibility.Flex) { 43 | self.axis = axis 44 | switch axis { 45 | case .horizontal: 46 | self.flexibility = Flexibility(horizontal: axisFlex, vertical: crossFlex) 47 | case .vertical: 48 | self.flexibility = Flexibility(horizontal: crossFlex, vertical: axisFlex) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Sources/Math/AxisPoint.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | 11 | /// A wrapper around CGPoint that makes it easy to do math relative to an axis. 12 | public struct AxisPoint { 13 | public let axis: Axis 14 | public var point: CGPoint 15 | 16 | public var axisOffset: CGFloat { 17 | set { 18 | switch axis { 19 | case .horizontal: 20 | point.x = newValue 21 | case .vertical: 22 | point.y = newValue 23 | } 24 | } 25 | get { 26 | switch axis { 27 | case .horizontal: 28 | return point.x 29 | case .vertical: 30 | return point.y 31 | } 32 | } 33 | } 34 | 35 | public var crossOffset: CGFloat { 36 | set { 37 | switch axis { 38 | case .horizontal: 39 | point.y = newValue 40 | case .vertical: 41 | point.x = newValue 42 | } 43 | } 44 | get { 45 | switch axis { 46 | case .horizontal: 47 | return point.y 48 | case .vertical: 49 | return point.x 50 | } 51 | } 52 | } 53 | 54 | public init(axis: Axis, point: CGPoint) { 55 | self.axis = axis 56 | self.point = point 57 | } 58 | 59 | public init(axis: Axis, axisOffset: CGFloat, crossOffset: CGFloat) { 60 | self.axis = axis 61 | switch axis { 62 | case .horizontal: 63 | self.point = CGPoint(x: axisOffset, y: crossOffset) 64 | case .vertical: 65 | self.point = CGPoint(x: crossOffset, y: axisOffset) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Sources/Math/AxisSize.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | 11 | /// A wrapper around CGSize that makes it easy to do math relative to an axis. 12 | public struct AxisSize { 13 | public let axis: Axis 14 | public var size: CGSize 15 | 16 | public var axisLength: CGFloat { 17 | set { 18 | switch axis { 19 | case .horizontal: 20 | size.width = newValue 21 | case .vertical: 22 | size.height = newValue 23 | } 24 | } 25 | get { 26 | switch axis { 27 | case .horizontal: 28 | return size.width 29 | case .vertical: 30 | return size.height 31 | } 32 | } 33 | } 34 | 35 | public var crossLength: CGFloat { 36 | set { 37 | switch axis { 38 | case .horizontal: 39 | size.height = newValue 40 | case .vertical: 41 | size.width = newValue 42 | } 43 | } 44 | get { 45 | switch axis { 46 | case .horizontal: 47 | return size.height 48 | case .vertical: 49 | return size.width 50 | } 51 | } 52 | } 53 | 54 | public init(axis: Axis, size: CGSize) { 55 | self.axis = axis 56 | self.size = size 57 | } 58 | 59 | public init(axis: Axis, axisLength: CGFloat, crossLength: CGFloat) { 60 | self.axis = axis 61 | switch axis { 62 | case .horizontal: 63 | self.size = CGSize(width: axisLength, height: crossLength) 64 | case .vertical: 65 | self.size = CGSize(width: crossLength, height: axisLength) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Sources/ObjCSupport/Builders/LOKInsetLayoutBuilder.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | #import 10 | 11 | #import "LOKLayoutBuilder.h" 12 | 13 | @class LOKInsetLayout; 14 | /** 15 | A layout builder for @c LOKInsetLayout. 16 | */ 17 | @interface LOKInsetLayoutBuilder: NSObject 18 | 19 | /** 20 | Creates a @c LOKInsetLayoutBuilder with the given insets and sublayout. 21 | @param insets The @c LOKEdgeInsets for layout. 22 | @param sublayout The layout placed inside the @c LOKInsetLayout. 23 | */ 24 | - (nonnull instancetype)initWithInsets:(LOKEdgeInsets)insets around:(nonnull id)sublayout; 25 | + (nonnull instancetype)withInsets:(LOKEdgeInsets)insets around:(nonnull id)sublayout; 26 | 27 | /** 28 | @c LOKInsetLayoutBuilder block for defining how this layout is positioned inside its parent layout. 29 | */ 30 | @property (nonatomic, nonnull, readonly) LOKInsetLayoutBuilder * _Nonnull(^alignment)(LOKAlignment * _Nullable); 31 | 32 | /** 33 | @c LOKInsetLayoutBuilder block for setting flexibility of the @c LOKInsetLayout. 34 | */ 35 | @property (nonatomic, nonnull, readonly) LOKInsetLayoutBuilder * _Nonnull(^flexibility)(LOKFlexibility * _Nullable); 36 | 37 | /** 38 | @c LOKInsetLayoutBuilder block for setting the viewReuseId used by LayoutKit. 39 | */ 40 | @property (nonatomic, nonnull, readonly) LOKInsetLayoutBuilder * _Nonnull(^viewReuseId)(NSString * _Nullable); 41 | 42 | /** 43 | @c LOKInsetLayoutBuilder block for setting the view class for the @c LOKInsetLayout. 44 | */ 45 | @property (nonatomic, nonnull, readonly) LOKInsetLayoutBuilder * _Nonnull(^viewClass)(Class _Nullable); 46 | 47 | /** 48 | Layoutkit configuration block called with created @c LOKView. 49 | */ 50 | @property (nonatomic, nonnull, readonly) LOKInsetLayoutBuilder * _Nonnull(^config)( void(^ _Nullable)(LOKView *_Nonnull)); 51 | 52 | /** 53 | @c LOKInsetLayoutBuilder block for setting edge inset for the @c LOKInsetLayout. 54 | */ 55 | @property (nonatomic, nonnull, readonly) LOKInsetLayoutBuilder * _Nonnull(^insets)(LOKEdgeInsets); 56 | 57 | /** 58 | Calling this builds and returns the @c LOKInsetLayout 59 | */ 60 | @property (nonatomic, nonnull, readonly) LOKInsetLayout *layout; 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/Builders/LOKLayoutBuilder.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | #import 10 | 11 | #if __has_include() 12 | // Importing the UI framework header. We could have just forward-declared `@class UIView` but `UIEdgeInsets` is a struct and cannot be forward-declared. 13 | #import 14 | typedef UIEdgeInsets LOKEdgeInsets; 15 | typedef UIView LOKView; 16 | #else 17 | #import 18 | typedef NSEdgeInsets LOKEdgeInsets; 19 | typedef NSView LOKView; 20 | #endif 21 | 22 | // Forward-declaring 23 | @class LOKAlignment; 24 | @class LOKFlexibility; 25 | @class LOKInsetLayoutBuilder; 26 | @protocol LOKLayout; 27 | 28 | /** 29 | This protocol's only purpose is to serve as a gentle reminder when implementing the builder 30 | to provide support for these common builder properties. This protocol probably shouldn't be 31 | used as a property, parameter, or return type. 32 | 33 | The protocol defines one method (@c layout) instead of a property because of issues 34 | in the ObjC/Swift interop. However, when you implement the protocol conformance in your 35 | builder you should use properties for everything. 36 | 37 | The types being returned (@c id and @c id) should be changed 38 | to the concrete type in your actual builder. It will still satisfy the protocol requirements 39 | but will allow the user of your builder to call the other builder-specific properties. 40 | */ 41 | @protocol LOKLayoutBuilder 42 | 43 | @property (nonatomic, nonnull, readonly) id _Nonnull(^alignment)(LOKAlignment * _Nullable); 44 | @property (nonatomic, nonnull, readonly) id _Nonnull(^flexibility)(LOKFlexibility * _Nullable); 45 | @property (nonatomic, nonnull, readonly) id _Nonnull(^viewReuseId)(NSString * _Nullable); 46 | @property (nonatomic, nonnull, readonly) id _Nonnull(^viewClass)(Class _Nullable); 47 | 48 | @property (nonatomic, nonnull, readonly) id _Nonnull(^config)( void(^ _Nullable)(LOKView *_Nonnull)); 49 | @property (nonatomic, nonnull, readonly) LOKInsetLayoutBuilder * _Nonnull(^insets)(LOKEdgeInsets); 50 | // This needs to be a function, because if it is a property it produces warnings, because 51 | // of shortcomings in the ObjC/Swift interop. When implemented, it should still be a property. 52 | - (nonnull id)layout; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/Internal/ReverseWrappedLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | 11 | class ReverseWrappedLayout: Layout { 12 | func arrangement(within rect: CGRect, measurement: LayoutMeasurement) -> LayoutArrangement { 13 | return layout.arrangement(within: rect, measurement: LOKLayoutMeasurement(wrappedLayout: layout, layoutMeasurement: measurement)).layoutArrangement 14 | } 15 | 16 | func measurement(within maxSize: CGSize) -> LayoutMeasurement { 17 | let measurement: LOKLayoutMeasurement = layout.measurement(within: maxSize) 18 | return LayoutMeasurement(layout: self, size: measurement.size, maxSize: measurement.maxSize, sublayouts: measurement.sublayouts.map { $0.measurement }) 19 | } 20 | 21 | var needsView: Bool { 22 | return layout.needsView 23 | } 24 | 25 | func makeView() -> View { 26 | return layout.makeView() 27 | } 28 | 29 | func configure(baseTypeView: View) { 30 | layout.configureView(baseTypeView) 31 | } 32 | 33 | var flexibility: Flexibility { 34 | return layout.flexibility.flexibility 35 | } 36 | 37 | var viewReuseId: String? { 38 | return layout.viewReuseId 39 | } 40 | 41 | let layout: LOKLayout 42 | init(layout: LOKLayout) { 43 | self.layout = layout 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/Internal/WrappedLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | 11 | class WrappedLayout: LOKLayout { 12 | func arrangement(within rect: CGRect, measurement: LOKLayoutMeasurement) -> LOKLayoutArrangement { 13 | let a = layout.arrangement( 14 | within: rect, 15 | measurement: measurement.measurement) 16 | return LOKLayoutArrangement(layoutArrangement: a) 17 | } 18 | 19 | func measurement(within maxSize: CGSize) -> LOKLayoutMeasurement { 20 | return LOKLayoutMeasurement(layoutMeasurement: layout.measurement(within: maxSize)) 21 | } 22 | 23 | var needsView: Bool { 24 | return layout.needsView 25 | } 26 | 27 | func makeView() -> View { 28 | return layout.makeView() 29 | } 30 | 31 | func configureView(_ view: View) { 32 | layout.configure(baseTypeView: view) 33 | } 34 | 35 | var flexibility: LOKFlexibility { 36 | return LOKFlexibility(flexibility: layout.flexibility) 37 | } 38 | 39 | var viewReuseId: String? { 40 | return layout.viewReuseId 41 | } 42 | 43 | let layout: Layout 44 | private init(layout: Layout) { 45 | self.layout = layout 46 | } 47 | 48 | static func wrap(layout: Layout) -> LOKLayout { 49 | if let reverseWrappedLayout = layout as? ReverseWrappedLayout { 50 | return reverseWrappedLayout.layout 51 | } else { 52 | return WrappedLayout(layout: layout) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/LOKAlignment.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | 11 | /** 12 | Specifies how a layout positions itself inside of the rect that it is given to it by its parent during arrangement. 13 | */ 14 | @objc open class LOKAlignment: NSObject { 15 | let alignment: Alignment 16 | init(alignment: Alignment) { 17 | self.alignment = alignment 18 | } 19 | @objc public static let center = LOKAlignment(alignment: .center) 20 | @objc public static let fill = LOKAlignment(alignment: .fill) 21 | @objc public static let topCenter = LOKAlignment(alignment: .topCenter) 22 | @objc public static let topFill = LOKAlignment(alignment: .topFill) 23 | @objc public static let topLeading = LOKAlignment(alignment: .topLeading) 24 | @objc public static let topTrailing = LOKAlignment(alignment: .topTrailing) 25 | @objc public static let bottomCenter = LOKAlignment(alignment: .bottomCenter) 26 | @objc public static let bottomFill = LOKAlignment(alignment: .bottomFill) 27 | @objc public static let bottomLeading = LOKAlignment(alignment: .bottomLeading) 28 | @objc public static let bottomTrailing = LOKAlignment(alignment: .bottomTrailing) 29 | @objc public static let centerFill = LOKAlignment(alignment: Alignment(vertical: .center, horizontal: .fill)) 30 | @objc public static let centerLeading = LOKAlignment(alignment: .centerLeading) 31 | @objc public static let centerTrailing = LOKAlignment(alignment: .centerTrailing) 32 | @objc public static let fillLeading = LOKAlignment(alignment: .fillLeading) 33 | @objc public static let fillTrailing = LOKAlignment(alignment: .fillTrailing) 34 | @objc public static let fillCenter = LOKAlignment(alignment: Alignment(vertical: .fill, horizontal: .center)) 35 | @objc public static let aspectFit = LOKAlignment(alignment: .aspectFit) 36 | 37 | /** 38 | Positions a rect of the given size inside the given rect using the alignment spec. 39 | */ 40 | @objc public func position(size: CGSize, in rect: CGRect) -> CGRect { 41 | return alignment.position(size: size, in: rect) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/LOKAnimation.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | /** 10 | An animation for a layout. 11 | */ 12 | @objc open class LOKAnimation: NSObject { 13 | private let animation: Animation 14 | 15 | init(animation: Animation) { 16 | self.animation = animation 17 | } 18 | 19 | /** 20 | Apply the final state of the animation. 21 | Call this inside a UIKit animation block. 22 | */ 23 | @objc public func apply() { 24 | animation.apply() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/LOKBatchUpdates.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | /** 12 | A set of updates to apply to a `ReloadableView`. 13 | 14 | Inherits from `NSObject` in order to be exposable to Objective-C. 15 | Objective-C exposability is needed in order to override methods from extensions that use `BatchUpdates` as parameter. 16 | */ 17 | @objc open class LOKBatchUpdates: NSObject { 18 | @objc public var insertItems = [IndexPath]() 19 | @objc public var deleteItems = [IndexPath]() 20 | @objc public var reloadItems = [IndexPath]() 21 | @objc public var moveItems = [LOKBatchUpdateItemMove]() 22 | 23 | @objc public var insertSections = IndexSet() 24 | @objc public var deleteSections = IndexSet() 25 | @objc public var reloadSections = IndexSet() 26 | @objc public var moveSections = [LOKBatchUpdateSectionMove]() 27 | 28 | @objc public override init() { 29 | super.init() 30 | } 31 | 32 | var unwrapped: BatchUpdates { 33 | let updates = BatchUpdates() 34 | updates.insertItems = insertItems 35 | updates.deleteItems = deleteItems 36 | updates.reloadItems = reloadItems 37 | updates.moveItems = moveItems.map { $0.unwrapped } 38 | updates.insertSections = insertSections 39 | updates.deleteSections = deleteSections 40 | updates.reloadSections = reloadSections 41 | updates.moveSections = moveSections.map { $0.unwrapped } 42 | return updates 43 | } 44 | } 45 | 46 | /** 47 | Instruction to move an item from one index path to another. 48 | */ 49 | @objc open class LOKBatchUpdateItemMove: NSObject { 50 | @objc public let from: IndexPath 51 | @objc public let to: IndexPath 52 | 53 | @objc public init(from: IndexPath, to: IndexPath) { 54 | self.from = from 55 | self.to = to 56 | } 57 | 58 | var unwrapped: ItemMove { 59 | return ItemMove(from: from, to: to) 60 | } 61 | } 62 | 63 | /** 64 | Instruction to move a section from one index to another. 65 | */ 66 | @objc open class LOKBatchUpdateSectionMove: NSObject { 67 | @objc public let from: Int 68 | @objc public let to: Int 69 | 70 | @objc public init(from: Int, to: Int) { 71 | self.from = from 72 | self.to = to 73 | } 74 | 75 | var unwrapped: SectionMove { 76 | return SectionMove(from: from, to: to) 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/LOKButtonLayoutType.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | /** 12 | Maps to `UIButtonType`. 13 | This prevents LayoutKit from breaking if a new `UIButtonType` is added. 14 | */ 15 | extension LOKButtonLayoutType { 16 | var unwrapped: ButtonLayoutType { 17 | switch self { 18 | case .custom: 19 | return .custom 20 | case .system: 21 | return .system 22 | case .detailDisclosure: 23 | return .detailDisclosure 24 | case .infoLight: 25 | return .infoLight 26 | case .infoDark: 27 | return .infoDark 28 | case .contactAdd: 29 | return .contactAdd 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/LOKFlexibility.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | /** 12 | The flexibility of a layout along both dimensions. 13 | 14 | Flexibility is a hint to a layout's parent about how the parent should prioritize space allocation among its children 15 | when there is either insufficient or too much space. 16 | 17 | A layout MAY use the flexibility of its sublayouts to determine how to allocate its available space between those sublayouts. 18 | A layout SHOULD NOT ever need to inspect its own flexiblity. 19 | 20 | A parent layout MAY compress ANY sublayout (even sublayouts that are configured as inflexible) if there is insufficient space. 21 | A parent layout MAY expand any flexible sublayout if there is excess space and if the parent layout wants to fill that space. 22 | A parent layout SHOULD favor expanding/compressing more flexible sublayouts over less flexible sublayouts. 23 | A parent layout SHOULD NOT expand inflexible sublayouts. 24 | */ 25 | @objc open class LOKFlexibility: NSObject { 26 | let flexibility: Flexibility 27 | 28 | init(flexibility: Flexibility) { 29 | self.flexibility = flexibility 30 | } 31 | 32 | /** 33 | The flexible flex value. 34 | */ 35 | @objc public static let flexible = LOKFlexibility(flexibility: .flexible) 36 | 37 | /** 38 | The inflexible flex value. 39 | */ 40 | @objc public static let inflexible = LOKFlexibility(flexibility: .inflexible) 41 | 42 | /** 43 | The minimum flex value that is still flexible. 44 | */ 45 | @objc public static let min = LOKFlexibility(flexibility: .min) 46 | 47 | /** 48 | The maximum flex value. 49 | */ 50 | @objc public static let max = LOKFlexibility(flexibility: .max) 51 | 52 | /** 53 | More flexible than the default flexibility. 54 | */ 55 | @objc public static let high = LOKFlexibility(flexibility: .high) 56 | 57 | /** 58 | Less flexible than the default flexibility. 59 | */ 60 | @objc public static let low = LOKFlexibility(flexibility: .low) 61 | 62 | @objc public static let horizontallyHighlyFlexible = LOKFlexibility(flexibility: Flexibility(horizontal: Flexibility.highFlex, vertical: Flexibility.inflexibleFlex)) 63 | @objc public static let horizontallyFlexible = LOKFlexibility(flexibility: Flexibility(horizontal: Flexibility.defaultFlex, vertical: Flexibility.inflexibleFlex)) 64 | @objc public static let verticallyFlexible = LOKFlexibility(flexibility: Flexibility(horizontal: Flexibility.inflexibleFlex, vertical: Flexibility.defaultFlex)) 65 | } 66 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/LOKInsetLayout.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | 11 | /** 12 | A layout that insets another layout. 13 | */ 14 | @objc open class LOKInsetLayout: LOKBaseLayout { 15 | 16 | /** 17 | `EdgeInsets` for layout. 18 | */ 19 | @objc public let insets: EdgeInsets 20 | 21 | /** 22 | Specifies how a layout positions itself inside of its parent view. 23 | */ 24 | @objc public let alignment: LOKAlignment 25 | 26 | /** 27 | Class object for the view class to be created. 28 | */ 29 | @objc public let viewClass: View.Type 30 | 31 | /** 32 | Sublayout for `LOKInsetLayout`. 33 | */ 34 | @objc public let sublayout: LOKLayout 35 | 36 | /** 37 | LayoutKit configuration block called with created View. 38 | */ 39 | @objc public let configure: ((View) -> Void)? 40 | 41 | @objc public init(insets: EdgeInsets, 42 | alignment: LOKAlignment? = nil, 43 | flexibility: LOKFlexibility? = nil, 44 | viewReuseId: String? = nil, 45 | viewClass: View.Type? = nil, 46 | sublayout: LOKLayout, 47 | configure: ((View) -> Void)? = nil) { 48 | self.insets = insets 49 | self.sublayout = sublayout 50 | self.alignment = alignment ?? .fill 51 | self.viewClass = viewClass ?? View.self 52 | self.configure = configure 53 | let layout = InsetLayout( 54 | insets: self.insets, 55 | alignment: self.alignment.alignment, 56 | flexibility: flexibility?.flexibility, 57 | viewReuseId: viewReuseId, 58 | sublayout: self.sublayout.unwrapped, 59 | viewClass: self.viewClass, 60 | config: self.configure) 61 | super.init(layout: layout) 62 | } 63 | 64 | @objc public class func inset(by insets: EdgeInsets, sublayout: LOKLayout) -> LOKInsetLayout { 65 | return LOKInsetLayout(insets: insets, sublayout: sublayout) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/LOKLayoutArrangementSection.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | @objc open class LOKLayoutArrangementSection: NSObject { 12 | let unwrapped: Section<[LayoutArrangement]> 13 | @objc public let header: LOKLayoutArrangement? 14 | @objc public let items: [LOKLayoutArrangement] 15 | @objc public let footer: LOKLayoutArrangement? 16 | @objc public init(header: LOKLayoutArrangement?, items: [LOKLayoutArrangement], footer: LOKLayoutArrangement?) { 17 | self.header = header 18 | self.items = items 19 | self.footer = footer 20 | unwrapped = Section(header: header?.layoutArrangement, items: items.map { $0.layoutArrangement }, footer: footer?.layoutArrangement) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/LOKLayoutMeasurement.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import CoreGraphics 10 | 11 | @objc open class LOKLayoutMeasurement: NSObject { 12 | let measurement: LayoutMeasurement 13 | private let wrappedLayout: LOKLayout 14 | 15 | /// The layout that was measured. 16 | @objc public var layout: LOKLayout { 17 | return wrappedLayout 18 | } 19 | 20 | /// The minimum size of the layout given the maximum size constraint. 21 | @objc public var size: CGSize { 22 | return measurement.size 23 | } 24 | 25 | /// The maximum size constraint used during measurement. 26 | @objc public var maxSize: CGSize { 27 | return measurement.maxSize 28 | } 29 | 30 | /// The measurements of the layout's sublayouts. 31 | @objc public var sublayouts: [LOKLayoutMeasurement] { 32 | return measurement.sublayouts.map { LOKLayoutMeasurement(layoutMeasurement: $0) } 33 | } 34 | 35 | @objc public init(layout: LOKLayout, size: CGSize, maxSize: CGSize, sublayouts: [LOKLayoutMeasurement]) { 36 | wrappedLayout = layout 37 | measurement = LayoutMeasurement(layout: layout.unwrapped, size: size, maxSize: maxSize, sublayouts: sublayouts.map { $0.measurement }) 38 | } 39 | 40 | init(layoutMeasurement: LayoutMeasurement) { 41 | wrappedLayout = WrappedLayout.wrap(layout: layoutMeasurement.layout) 42 | measurement = layoutMeasurement 43 | } 44 | 45 | init(wrappedLayout: LOKLayout, layoutMeasurement: LayoutMeasurement) { 46 | self.wrappedLayout = wrappedLayout 47 | measurement = layoutMeasurement 48 | } 49 | 50 | /// Convenience method to position this measured layout. 51 | @objc public func arrangement(within rect: CGRect) -> LOKLayoutArrangement { 52 | return LOKLayoutArrangement(layoutArrangement: measurement.arrangement(within: rect)) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Sources/ObjCSupport/LOKLayoutSection.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | @objc open class LOKLayoutSection: NSObject { 12 | let unwrapped: Section<[Layout]> 13 | @objc public init(header: LOKLayout?, items: [LOKLayout], footer: LOKLayout?) { 14 | unwrapped = Section(header: header?.unwrapped, items: items.map { $0.unwrapped }, footer: footer?.unwrapped) 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Sources/UIKitSupport.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | public typealias View = UIView 12 | 13 | public typealias EdgeInsets = UIEdgeInsets 14 | 15 | public typealias UserInterfaceLayoutDirection = UIUserInterfaceLayoutDirection 16 | 17 | extension UIView { 18 | 19 | func convertToAbsoluteCoordinates(_ rect: CGRect) -> CGRect { 20 | return convert(rect, to: UIScreen.main.fixedCoordinateSpace) 21 | } 22 | 23 | func convertFromAbsoluteCoordinates(_ rect: CGRect) -> CGRect { 24 | return convert(rect, from: UIScreen.main.fixedCoordinateSpace) 25 | } 26 | 27 | /// Expose API that is identical to NSView. 28 | var userInterfaceLayoutDirection: UIUserInterfaceLayoutDirection { 29 | if #available(iOS 9.0, *) { 30 | return UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) 31 | } else { 32 | // Before iOS 9, there wasn't good support for RTL interfaces 33 | // (even the OS itself didn't swap interfaces right to left). 34 | // The best we can do is check the language direction of the preferred localization 35 | // and use that. 36 | if let isoLangCode = Bundle.main.preferredLocalizations.first { 37 | switch NSLocale.characterDirection(forLanguage: isoLangCode) { 38 | case .unknown, .leftToRight, .topToBottom, .bottomToTop: 39 | return .leftToRight 40 | case .rightToLeft: 41 | return .rightToLeft 42 | @unknown default: 43 | return .leftToRight 44 | } 45 | } else { 46 | #if LAYOUTKIT_EXTENSION_DEFAULT_RIGHT_TO_LEFT 47 | return .rightToLeft 48 | #else 49 | return .leftToRight 50 | #endif 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Views/BatchUpdates.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Foundation 10 | 11 | 12 | /** 13 | A set of updates to apply to a `ReloadableView`. 14 | 15 | Inherits from NSObject in order to be exposable to Objective-C. 16 | Objective-C exposability is needed in order to override methods from extensions that use `BatchUpdates` as parameter. 17 | */ 18 | public class BatchUpdates: NSObject { 19 | public var insertItems = [IndexPath]() 20 | public var deleteItems = [IndexPath]() 21 | public var reloadItems = [IndexPath]() 22 | public var moveItems = [ItemMove]() 23 | 24 | public var insertSections = IndexSet() 25 | public var deleteSections = IndexSet() 26 | public var reloadSections = IndexSet() 27 | public var moveSections = [SectionMove]() 28 | 29 | public override init() { 30 | super.init() 31 | } 32 | } 33 | 34 | /// Instruction to move an item from one index path to another. 35 | public struct ItemMove: Equatable { 36 | public let from: IndexPath 37 | public let to: IndexPath 38 | 39 | public init(from: IndexPath, to: IndexPath) { 40 | self.from = from 41 | self.to = to 42 | } 43 | } 44 | 45 | public func ==(lhs: ItemMove, rhs: ItemMove) -> Bool { 46 | return lhs.from == rhs.from && lhs.to == rhs.to 47 | } 48 | 49 | /// Instruction to move a section from one index to another. 50 | public struct SectionMove: Equatable { 51 | public let from: Int 52 | public let to: Int 53 | 54 | public init(from: Int, to: Int) { 55 | self.from = from 56 | self.to = to 57 | } 58 | } 59 | 60 | public func ==(lhs: SectionMove, rhs: SectionMove) -> Bool { 61 | return lhs.from == rhs.from && lhs.to == rhs.to 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Views/LayoutAdapterCollectionView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | /** 12 | A UICollectionView that retains and uses a ReloadableViewLayoutAdapter as its delegate and data source. 13 | */ 14 | open class LayoutAdapterCollectionView: UICollectionView { 15 | open lazy var layoutAdapter: ReloadableViewLayoutAdapter = { 16 | let adapter = ReloadableViewLayoutAdapter(reloadableView: self) 17 | self.dataSource = adapter 18 | self.delegate = adapter 19 | return adapter 20 | }() 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Views/LayoutAdapterTableView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | 11 | /** 12 | A UITableView that retains and uses a ReloadableViewLayoutAdapter as its delegate and data source. 13 | */ 14 | open class LayoutAdapterTableView: UITableView { 15 | open lazy var layoutAdapter: ReloadableViewLayoutAdapter = { 16 | let adapter = ReloadableViewLayoutAdapter(reloadableView: self) 17 | self.dataSource = adapter 18 | self.delegate = adapter 19 | return adapter 20 | }() 21 | } 22 | -------------------------------------------------------------------------------- /Tests/cocoapods/ios/LayoutKit-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/cocoapods/ios/LayoutKit-iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 19 | // Override point for customization after application launch. 20 | return true 21 | } 22 | 23 | func applicationWillResignActive(_ application: UIApplication) { 24 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 25 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 26 | } 27 | 28 | func applicationDidEnterBackground(_ application: UIApplication) { 29 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 31 | } 32 | 33 | func applicationWillEnterForeground(_ application: UIApplication) { 34 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 35 | } 36 | 37 | func applicationDidBecomeActive(_ application: UIApplication) { 38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 39 | } 40 | 41 | func applicationWillTerminate(_ application: UIApplication) { 42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 43 | } 44 | 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Tests/cocoapods/ios/LayoutKit-iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIRequiredDeviceCapabilities 24 | 25 | armv7 26 | 27 | UISupportedInterfaceOrientations 28 | 29 | UIInterfaceOrientationPortrait 30 | UIInterfaceOrientationLandscapeLeft 31 | UIInterfaceOrientationLandscapeRight 32 | 33 | UISupportedInterfaceOrientations~ipad 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationPortraitUpsideDown 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Tests/cocoapods/ios/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '8.0' 3 | 4 | use_frameworks! 5 | 6 | target 'LayoutKit-iOS' do 7 | pod 'LayoutKit' 8 | end 9 | -------------------------------------------------------------------------------- /Tests/cocoapods/macos/LayoutKit-macOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/cocoapods/macos/LayoutKit-macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import Cocoa 10 | import LayoutKit 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate { 14 | 15 | 16 | 17 | func applicationDidFinishLaunching(_ aNotification: Notification) { 18 | // Insert code here to initialize your application 19 | } 20 | 21 | func applicationWillTerminate(_ aNotification: Notification) { 22 | // Insert code here to tear down your application 23 | } 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Tests/cocoapods/macos/LayoutKit-macOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Tests/cocoapods/macos/LayoutKit-macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2017 Linkedin. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /Tests/cocoapods/macos/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :osx, '10.11' 3 | 4 | use_frameworks! 5 | 6 | target 'LayoutKit-macOS' do 7 | pod 'LayoutKit' 8 | end 9 | -------------------------------------------------------------------------------- /Tests/cocoapods/tvos/LayoutKit-tvOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/cocoapods/tvos/LayoutKit-tvOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 LinkedIn Corp. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | // 5 | // Unless required by applicable law or agreed to in writing, 6 | // software distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | 9 | import UIKit 10 | import LayoutKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 19 | // Override point for customization after application launch. 20 | return true 21 | } 22 | 23 | func applicationWillResignActive(_ application: UIApplication) { 24 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 25 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 26 | } 27 | 28 | func applicationDidEnterBackground(_ application: UIApplication) { 29 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 31 | } 32 | 33 | func applicationWillEnterForeground(_ application: UIApplication) { 34 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 35 | } 36 | 37 | func applicationDidBecomeActive(_ application: UIApplication) { 38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 39 | } 40 | 41 | func applicationWillTerminate(_ application: UIApplication) { 42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 43 | } 44 | 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Tests/cocoapods/tvos/LayoutKit-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIMainStoryboardFile 24 | Main 25 | UIRequiredDeviceCapabilities 26 | 27 | arm64 28 | 29 | UIUserInterfaceStyle 30 | Automatic 31 | 32 | 33 | -------------------------------------------------------------------------------- /Tests/cocoapods/tvos/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :tvos, '10.2' 3 | 4 | use_frameworks! 5 | 6 | target 'LayoutKit-tvOS' do 7 | pod 'LayoutKit' 8 | end 9 | -------------------------------------------------------------------------------- /Tests/swift-package-manager/ios/LayoutKitSampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/swift-package-manager/ios/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '8.0' 3 | 4 | target 'LayoutKitSampleApp' do 5 | pod 'LayoutKit' 6 | use_frameworks! 7 | end 8 | -------------------------------------------------------------------------------- /debug-time-function-bodies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Pipe the output of xcodebuild to this script to see the slowest 10 functions to compile. 4 | # Use this flag with xcodebuild: OTHER_SWIFT_FLAGS='-Xfrontend -debug-time-function-bodies' 5 | 6 | echo "\nSlowest functions to compile:\n" 7 | grep '^[0-9]\+\.[0-9]\+' | sed 's/\(^[0-9]*.[0-9]*\)\([a-z]*\)/\1 \2/' | sort -k2,2r -k1,1nr | sed 's/\(^[0-9]*.[0-9]*\) \([a-z]*\)/\1\2/' | head -n 10 | sed 's/^/ /' 8 | echo "\n" 9 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | layoutkit.org 2 | -------------------------------------------------------------------------------- /docs/benchmarks.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | This page contains benchmark results for LayoutKit, Auto Layout, and manual layout code. 4 | 5 | > LayoutKit is as fast as manual layout code and significantly faster than Auto Layout. 6 | 7 | ## Methodology 8 | 9 | Benchmarks were run on an iPhone 6 running iOS 9.3.2 with Swift optimization turned on. 10 | 11 | ## UICollectionView 12 | 13 | ![UICollectionView benchmark](img/collectionview-benchmark.png) 14 | 15 | Notes: 16 | 17 | - `estimatedItemSize` is [hard to use](http://stackoverflow.com/questions/26143591/specifying-one-dimension-of-cells-in-uicollectionview-using-auto-layout/26349770#26349770) so these tests use `sizeForItemAtIndexPath`. 18 | - UICollectionViewFlowLayout requests the height of all cells during layout (even those that are off screen). This is why layout performance keeps getting worse as number of cells are added to the UICollectionView. 19 | 20 | ## UITableView 21 | 22 | ![UITableView benchmark](img/tableview-benchmark.png) 23 | 24 | Notes: 25 | 26 | - Unlike UICollectionView, UITableView defers asking for the height of cells until it is needed. This is why performance is constant for seven or more cells. 27 | -------------------------------------------------------------------------------- /docs/code-documentation.md: -------------------------------------------------------------------------------- 1 | 2 | Redirecting to code documentation. 3 | -------------------------------------------------------------------------------- /docs/custom-layouts.md: -------------------------------------------------------------------------------- 1 | # Custom layouts 2 | 3 | This page is an overview of how to create a custom layout. 4 | 5 | ## When to create a custom layout 6 | 7 | Only create a custom layout if your UIs can't be expressed by composing the basic layouts that LayoutKit provides. 8 | 9 | ## Layout protocol 10 | 11 | Create a custom layout by implementing the [Layout](https://github.com/linkedin/LayoutKit/blob/master/Sources/Layout.swift) protocol. 12 | 13 | Please read the documentation of the protocol and its methods carefully. 14 | -------------------------------------------------------------------------------- /docs/img/animation-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/docs/img/animation-example.gif -------------------------------------------------------------------------------- /docs/img/collectionview-benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/docs/img/collectionview-benchmark.png -------------------------------------------------------------------------------- /docs/img/helloworld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/docs/img/helloworld.png -------------------------------------------------------------------------------- /docs/img/layoutkit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 14 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 34 | 36 | 37 | -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/docs/img/logo.png -------------------------------------------------------------------------------- /docs/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/img/nick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/docs/img/nick.png -------------------------------------------------------------------------------- /docs/img/sergei.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/docs/img/sergei.png -------------------------------------------------------------------------------- /docs/img/tableview-benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/LayoutKit/1f1b067b6b11a0779ea6b6ccd976585a1d7bb79b/docs/img/tableview-benchmark.png -------------------------------------------------------------------------------- /docs/layoutkit.css: -------------------------------------------------------------------------------- 1 | .navbar-brand { 2 | background-image: url('img/logo.svg'); 3 | background-repeat: no-repeat; 4 | padding-left: 60px; 5 | } 6 | 7 | div[role='main'] p:nth-child(-n+2) img { 8 | background-color: transparent; 9 | border: none; 10 | margin: 0px; 11 | } 12 | 13 | div[role='main'] p:first-child { 14 | margin-bottom: 0; 15 | } 16 | 17 | div[role='main'] p:nth-child(2) { 18 | margin-bottom: 20px; 19 | } 20 | 21 | a { 22 | color: #4285C5; 23 | } 24 | 25 | .navbar { 26 | background-image: -webkit-linear-gradient(#178DCA,#4285C5); 27 | background-image: linear-gradient(#178DCA,#4285C5); 28 | } 29 | 30 | /* Replace #178acc with #107ABF from bootstrap theme */ 31 | .text-primary:hover{color:#107ABF} 32 | .navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#fff;background-color:#107ABF} 33 | .navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;background-color:#107ABF} 34 | .navbar-default .navbar-toggle{border-color:#107ABF} 35 | .navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#107ABF} 36 | .navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#fff;background-color:#107ABF} 37 | .navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:#107ABF} 38 | .navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#107ABF} 39 | .label-primary[href]:hover,.label-primary[href]:focus{background-color:#107ABF} 40 | -------------------------------------------------------------------------------- /docs/uikit.md: -------------------------------------------------------------------------------- 1 | # Interfacing with UIKit 2 | 3 | This page guides you through the process of interfacing with UIKit. 4 | 5 | ## Threading 6 | 7 | There are three steps to the layout process. 8 | 9 | 1. Instantiate a layout object. 10 | 2. Compute the layout's view frames. 11 | 3. Instantiate views and assign frames. 12 | 13 | Unlike UIKit, LayoutKit can perform steps #1 and #2 on a background thread, but doing so is completely optional. 14 | 15 | Examples: 16 | 17 | - [BackgroundMiniProfileViewController](https://github.com/linkedin/LayoutKit/blob/master/LayoutKitSampleApp/BackgroundMiniProfileViewController.swift) 18 | - [ForegroundMiniProfileViewController](https://github.com/linkedin/LayoutKit/blob/master/LayoutKitSampleApp/ForegroundMiniProfileViewController.swift) 19 | 20 | LayoutKit is faster than Auto Layout by default so it is perfectly fine to not bother with background layout if performance on the main thread is acceptable. 21 | 22 | ## UICollectionView and UITableView 23 | 24 | If you have a UICollectionView or UITableView and all of the cells use LayoutKit, then you can use [ReloadableViewLayoutAdapter](https://github.com/linkedin/LayoutKit/blob/master/Sources/Views/ReloadableViewLayoutAdapter.swift) to automatically handle computing cell layouts on a background thread. 25 | 26 | ## Mixing Auto Layout and LayoutKit 27 | 28 | If you have a UI that mixes LayoutKit and Auto Layout (e.g. some cells use LayoutKit and others use Auto Layout), then you may want to avoid the additional complexity of background layout. 29 | 30 | Instead, perform all layout computations on the main thread (similar to how [StackView](https://github.com/linkedin/LayoutKit/blob/master/Sources/Views/StackView.swift) is implemented). 31 | 32 | ## StackView 33 | 34 | If you don't want to think about layouts or threading, [StackView](https://github.com/linkedin/LayoutKit/blob/master/Sources/Views/StackView.swift) is an easy way to start taking advantage of the performance of LayoutKit. 35 | 36 | - It is similar to UIStackView except it uses LayoutKit's StackLayout algorithm to efficiently stack subviews. 37 | - It is faster than UIStackView and it is also faster than manually stacking views with Auto Layout. 38 | 39 | You can use StackView like any other UIView, but there are a few extra considerations that you need to be aware of 40 | if you want to use it with Auto Layout (please read StackView's class documentation). 41 | 42 | ## Summary 43 | 44 | We recommend choosing the simplest option that works with any existing code that you have and achieves acceptable performance. 45 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: LayoutKit 2 | site_description: LayoutKit is a fast view layout library for iOS, macOS, and tvOS. 3 | site_author: LinkedIn 4 | site_favicon: img/logo.png 5 | repo_url: https://github.com/linkedin/LayoutKit 6 | extra_css: [layoutkit.css] 7 | 8 | pages: 9 | - Home: index.md 10 | - Guide: 11 | - Building UI: building-ui.md 12 | - Custom Layouts: custom-layouts.md 13 | - Interfacing with UIKit: uikit.md 14 | - Animations: animations.md 15 | - Code documentation: code-documentation.md 16 | - About: 17 | - Benchmarks: benchmarks.md 18 | 19 | copyright: Copyright © 2016 LinkedIn 20 | google_analytics: ['UA-79736636-1', 'layoutkit.org'] 21 | --------------------------------------------------------------------------------