Push from command line

This commit is contained in:
Deniz Duezgoeren
2019-08-12 11:20:21 +02:00
parent 3a919dcb23
commit f1345eac14
512 changed files with 103288 additions and 1930 deletions
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,37 @@
// Created by bryankeller on 10/16/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
import Foundation
/// Represents the layout information for a background in a section.
struct BackgroundModel {
// MARK: Lifecycle
init() {
id = NSUUID().uuidString
originInSection = .zero
size = .zero
}
// MARK: Internal
let id: String
var originInSection: CGPoint
var size: CGSize
}
@@ -0,0 +1,38 @@
// Created by Roman Laitarenko on 1/31/19.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
import Foundation
/// Represents the layout information for a footer in a section.
struct FooterModel {
// MARK: Lifecycle
init(heightMode: MagazineLayoutFooterHeightMode, height: CGFloat, pinToVisibleBounds: Bool) {
self.heightMode = heightMode
self.pinToVisibleBounds = pinToVisibleBounds
originInSection = .zero
size = CGSize(width: 0, height: height)
}
// MARK: Internal
var heightMode: MagazineLayoutFooterHeightMode
var pinToVisibleBounds: Bool
var originInSection: CGPoint
var size: CGSize
var preferredHeight: CGFloat?
}
+39
View File
@@ -0,0 +1,39 @@
// Created by bryankeller on 10/16/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
import Foundation
/// Represents the layout information for a header in a section.
struct HeaderModel {
// MARK: Lifecycle
init(heightMode: MagazineLayoutHeaderHeightMode, height: CGFloat, pinToVisibleBounds: Bool) {
self.heightMode = heightMode
self.pinToVisibleBounds = pinToVisibleBounds
originInSection = .zero
size = CGSize(width: 0, height: height)
}
// MARK: Internal
var heightMode: MagazineLayoutHeaderHeightMode
var pinToVisibleBounds: Bool
var originInSection: CGPoint
var size: CGSize
var preferredHeight: CGFloat?
}
@@ -0,0 +1,40 @@
// Created by bryankeller on 7/9/17.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
import Foundation
/// Represents the layout information for an item in a section.
struct ItemModel {
// MARK: Lifecycle
init(sizeMode: MagazineLayoutItemSizeMode, height: CGFloat) {
id = NSUUID().uuidString
self.sizeMode = sizeMode
originInSection = .zero
size = CGSize(width: 0, height: height)
}
// MARK: Internal
let id: String
var sizeMode: MagazineLayoutItemSizeMode
var originInSection: CGPoint
var size: CGSize
var preferredHeight: CGFloat?
}
File diff suppressed because it is too large Load Diff
+640
View File
@@ -0,0 +1,640 @@
// Created by bryankeller on 7/9/17.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
import Foundation
/// Represents the layout information for a section.
struct SectionModel {
// MARK: Lifecycle
init(
itemModels: [ItemModel],
headerModel: HeaderModel?,
footerModel: FooterModel?,
backgroundModel: BackgroundModel?,
metrics: MagazineLayoutSectionMetrics)
{
id = NSUUID().uuidString
self.itemModels = itemModels
self.headerModel = headerModel
self.footerModel = footerModel
self.backgroundModel = backgroundModel
self.metrics = metrics
calculatedHeight = 0
numberOfRows = 0
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: 0)
calculateElementFramesIfNecessary()
}
// MARK: Internal
let id: String
private(set) var headerModel: HeaderModel?
private(set) var footerModel: FooterModel?
private(set) var backgroundModel: BackgroundModel?
var visibleBounds: CGRect?
var numberOfItems: Int {
return itemModels.count
}
func idForItemModel(atIndex index: Int) -> String {
return itemModels[index].id
}
func indexForItemModel(withID id: String) -> Int? {
return itemModels.index { $0.id == id }
}
func itemModel(atIndex index: Int) -> ItemModel {
return itemModels[index]
}
func preferredHeightForItemModel(atIndex index: Int) -> CGFloat? {
return itemModels[index].preferredHeight
}
mutating func calculateHeight() -> CGFloat {
calculateElementFramesIfNecessary()
return calculatedHeight
}
mutating func calculateFrameForItem(atIndex index: Int) -> CGRect {
calculateElementFramesIfNecessary()
var origin = itemModels[index].originInSection
if let rowIndex = rowIndicesForItemIndices[index] {
origin.y += rowOffsetTracker?.offsetForRow(at: rowIndex) ?? 0
} else {
assertionFailure("Expected a row and a row height for item at \(index).")
}
return CGRect(origin: origin, size: itemModels[index].size)
}
mutating func calculateFrameForHeader(
inSectionVisibleBounds sectionVisibleBounds: CGRect)
-> CGRect?
{
guard headerModel != nil else { return nil }
calculateElementFramesIfNecessary()
// `headerModel` is a value type that might be mutated in `calculateElementFramesIfNecessary`,
// so we can't use a copy made before that code executes (for example, in a
// `guard let headerModel = headerModel else { ... }` at the top of this function).
if let headerModel = headerModel {
let originY: CGFloat
if headerModel.pinToVisibleBounds {
originY = max(
min(
sectionVisibleBounds.minY,
calculateHeight() -
metrics.sectionInsets.bottom -
(footerModel?.size.height ?? 0) -
headerModel.size.height),
headerModel.originInSection.y)
} else {
originY = headerModel.originInSection.y
}
return CGRect(
origin: CGPoint(x: headerModel.originInSection.x, y: originY),
size: headerModel.size)
} else {
return nil
}
}
mutating func calculateFrameForFooter(
inSectionVisibleBounds sectionVisibleBounds: CGRect)
-> CGRect?
{
guard footerModel != nil else { return nil }
calculateElementFramesIfNecessary()
var origin = footerModel?.originInSection
if let rowIndex = indexOfFooterRow() {
origin?.y += rowOffsetTracker?.offsetForRow(at: rowIndex) ?? 0
} else {
assertionFailure("Expected a row and a corresponding section footer.")
}
// `footerModel` is a value type that might be mutated in `calculateElementFramesIfNecessary`,
// so we can't use a copy made before that code executes (for example, in a
// `guard let footerModel = footerModel else { ... }` at the top of this function).
if let footerModel = footerModel, let origin = origin {
let originY: CGFloat
if footerModel.pinToVisibleBounds {
originY = min(
max(
sectionVisibleBounds.maxY - footerModel.size.height,
metrics.sectionInsets.top + (headerModel?.size.height ?? 0)),
origin.y)
} else {
originY = origin.y
}
return CGRect(
origin: CGPoint(x: footerModel.originInSection.x, y: originY),
size: footerModel.size)
} else {
return nil
}
}
mutating func calculateFrameForBackground() -> CGRect? {
let calculatedHeight = calculateHeight()
backgroundModel?.originInSection = CGPoint(
x: metrics.sectionInsets.left,
y: metrics.sectionInsets.top)
backgroundModel?.size.width = metrics.width
backgroundModel?.size.height = calculatedHeight -
metrics.sectionInsets.top -
metrics.sectionInsets.bottom
if let backgroundModel = backgroundModel {
return CGRect(
origin: CGPoint(x: backgroundModel.originInSection.x, y: backgroundModel.originInSection.y),
size: backgroundModel.size)
} else {
return nil
}
}
@discardableResult
mutating func deleteItemModel(atIndex indexOfDeletion: Int) -> ItemModel {
updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex: indexOfDeletion)
return itemModels.remove(at: indexOfDeletion)
}
mutating func insert(_ itemModel: ItemModel, atIndex indexOfInsertion: Int) {
updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex: indexOfInsertion)
itemModels.insert(itemModel, at: indexOfInsertion)
}
mutating func updateMetrics(to metrics: MagazineLayoutSectionMetrics) {
guard self.metrics != metrics else { return }
self.metrics = metrics
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: 0)
}
mutating func updateItemSizeMode(to sizeMode: MagazineLayoutItemSizeMode, atIndex index: Int) {
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
// and Swift retain / release calls.
let itemModelsPointer = UnsafeMutableRawPointer(mutating: &itemModels)
let directlyMutableItemModels = itemModelsPointer.assumingMemoryBound(to: ItemModel.self)
directlyMutableItemModels[index].sizeMode = sizeMode
if case let .static(staticHeight) = sizeMode.heightMode {
directlyMutableItemModels[index].size.height = staticHeight
}
updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex: index)
}
mutating func setHeader(_ headerModel: HeaderModel) {
let oldPreferredHeight = self.headerModel?.preferredHeight
self.headerModel = headerModel
if case let .static(staticHeight) = headerModel.heightMode {
self.headerModel?.size.height = staticHeight
} else if case .dynamic = headerModel.heightMode {
self.headerModel?.preferredHeight = oldPreferredHeight
}
if let indexOfHeader = indexOfHeaderRow() {
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfHeader)
}
}
mutating func setFooter(_ footerModel: FooterModel) {
let oldPreferredHeight = self.footerModel?.preferredHeight
self.footerModel = footerModel
if case let .static(staticHeight) = footerModel.heightMode {
self.footerModel?.size.height = staticHeight
} else if case .dynamic = footerModel.heightMode {
self.footerModel?.preferredHeight = oldPreferredHeight
}
if let indexOfFooter = indexOfFooterRow() {
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfFooter)
}
}
mutating func removeHeader() {
if let indexOfHeader = indexOfHeaderRow() {
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfHeader)
}
headerModel = nil
}
mutating func removeFooter() {
if let indexOfFooter = indexOfFooterRow() {
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfFooter)
}
footerModel = nil
}
mutating func updateItemHeight(toPreferredHeight preferredHeight: CGFloat, atIndex index: Int) {
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
// and Swift retain / release calls.
let itemModelsPointer = UnsafeMutableRawPointer(mutating: &itemModels)
let directlyMutableItemModels = itemModelsPointer.assumingMemoryBound(to: ItemModel.self)
directlyMutableItemModels[index].preferredHeight = preferredHeight
if
let rowIndex = rowIndicesForItemIndices[index],
let rowHeight = itemRowHeightsForRowIndices[rowIndex]
{
let newRowHeight = updateHeightsForItemsInRow(at: rowIndex)
let heightDelta = newRowHeight - rowHeight
calculatedHeight += heightDelta
let firstAffectedRowIndex = rowIndex + 1
if firstAffectedRowIndex < numberOfRows {
rowOffsetTracker?.addOffset(heightDelta, forRowsStartingAt: firstAffectedRowIndex)
}
} else {
assertionFailure("Expected a row and a row height for item at \(index).")
return
}
}
mutating func updateHeaderHeight(toPreferredHeight preferredHeight: CGFloat) {
headerModel?.preferredHeight = preferredHeight
if let indexOfHeaderRow = indexOfHeaderRow(), let headerModel = headerModel {
let rowHeight = headerModel.size.height
let newRowHeight = updateHeaderHeight(withMetricsFrom: headerModel)
let heightDelta = newRowHeight - rowHeight
calculatedHeight += heightDelta
let firstAffectedRowIndex = indexOfHeaderRow + 1
if firstAffectedRowIndex < numberOfRows {
rowOffsetTracker?.addOffset(heightDelta, forRowsStartingAt: firstAffectedRowIndex)
}
} else {
assertionFailure("Expected a row, a row height, and a corresponding section header.")
return
}
}
mutating func updateFooterHeight(toPreferredHeight preferredHeight: CGFloat) {
footerModel?.preferredHeight = preferredHeight
if let indexOfFooterRow = indexOfFooterRow(), let footerModel = footerModel {
let rowHeight = footerModel.size.height
let newRowHeight = updateFooterHeight(withMetricsFrom: footerModel)
let heightDelta = newRowHeight - rowHeight
calculatedHeight += heightDelta
let firstAffectedRowIndex = indexOfFooterRow + 1
if firstAffectedRowIndex < numberOfRows {
rowOffsetTracker?.addOffset(heightDelta, forRowsStartingAt: firstAffectedRowIndex)
}
} else {
assertionFailure("Expected a row, a row height, and a corresponding section footer.")
return
}
}
mutating func setBackground(_ backgroundModel: BackgroundModel) {
self.backgroundModel = backgroundModel
// No need to invalidate since the background doesn't affect the layout.
}
mutating func removeBackground() {
backgroundModel = nil
// No need to invalidate since the background doesn't affect the layout.
}
// MARK: Private
private var numberOfRows: Int
private var itemModels: [ItemModel]
private var metrics: MagazineLayoutSectionMetrics
private var calculatedHeight: CGFloat
private var indexOfFirstInvalidatedRow: Int? {
didSet {
guard indexOfFirstInvalidatedRow != nil else { return }
applyRowOffsetsIfNecessary()
}
}
private var itemIndicesForRowIndices = [Int: [Int]]()
private var rowIndicesForItemIndices = [Int: Int]()
private var itemRowHeightsForRowIndices = [Int: CGFloat]()
private var rowOffsetTracker: RowOffsetTracker?
private func maxYForItemsRow(atIndex rowIndex: Int) -> CGFloat? {
guard
let itemIndices = itemIndicesForRowIndices[rowIndex],
let itemY = itemIndices.first.flatMap({ itemModels[$0].originInSection.y }),
let itemHeight = itemIndices.map({ itemModels[$0].size.height }).max() else
{
return nil
}
return itemY + itemHeight
}
private func indexOfHeaderRow() -> Int? {
guard headerModel != nil else { return nil }
return 0
}
private func indexOfFirstItemsRow() -> Int? {
guard numberOfItems > 0 else { return nil }
return headerModel == nil ? 0 : 1
}
private func indexOfLastItemsRow() -> Int? {
guard numberOfItems > 0 else { return nil }
return rowIndicesForItemIndices[numberOfItems - 1]
}
private func indexOfFooterRow() -> Int? {
guard footerModel != nil else { return nil }
return numberOfRows - 1
}
private mutating func updateIndexOfFirstInvalidatedRow(forChangeToItemAtIndex changedIndex: Int) {
guard
let indexOfCurrentRow = rowIndicesForItemIndices[changedIndex],
indexOfCurrentRow > 0 else
{
indexOfFirstInvalidatedRow = rowIndicesForItemIndices[0] ?? 0
return
}
updateIndexOfFirstInvalidatedRowIfNecessary(toProposedIndex: indexOfCurrentRow - 1)
}
private mutating func updateIndexOfFirstInvalidatedRowIfNecessary(
toProposedIndex proposedIndex: Int)
{
indexOfFirstInvalidatedRow = min(proposedIndex, indexOfFirstInvalidatedRow ?? proposedIndex)
}
private mutating func applyRowOffsetsIfNecessary() {
guard let rowOffsetTracker = rowOffsetTracker else { return }
for rowIndex in 0..<numberOfRows {
let rowOffset = rowOffsetTracker.offsetForRow(at: rowIndex)
switch rowIndex {
case indexOfHeaderRow(): headerModel?.originInSection.y += rowOffset
case indexOfFooterRow(): footerModel?.originInSection.y += rowOffset
default:
for itemIndex in itemIndicesForRowIndices[rowIndex] ?? [] {
itemModels[itemIndex].originInSection.y += rowOffset
}
}
}
self.rowOffsetTracker = nil
}
private mutating func calculateElementFramesIfNecessary() {
guard var rowIndex = indexOfFirstInvalidatedRow else { return }
guard rowIndex >= 0 else {
assertionFailure("Invalid `rowIndex` / `indexOfFirstInvalidatedRow` (\(rowIndex)).")
return
}
// Clean up item / row / height mappings starting at our `indexOfFirstInvalidatedRow`; we'll
// make new mappings for those row indices as we do layout calculations below. Since all
// item / row index mappings before `indexOfFirstInvalidatedRow` are still valid, we'll leave
// those alone.
for rowIndexKey in itemIndicesForRowIndices.keys {
guard rowIndexKey >= rowIndex else { continue }
if let itemIndex = itemIndicesForRowIndices[rowIndexKey]?.first {
rowIndicesForItemIndices[itemIndex] = nil
}
itemIndicesForRowIndices[rowIndexKey] = nil
itemRowHeightsForRowIndices[rowIndex] = nil
}
// Header frame calculation
if rowIndex == indexOfHeaderRow(), let existingHeaderModel = headerModel {
rowIndex = 1
headerModel?.originInSection = CGPoint(
x: metrics.sectionInsets.left,
y: metrics.sectionInsets.top)
headerModel?.size.width = metrics.width
updateHeaderHeight(withMetricsFrom: existingHeaderModel)
}
var currentY: CGFloat
// Item frame calculations
let startingItemIndex: Int
if
let indexOfLastItemInPreviousRow = itemIndicesForRowIndices[rowIndex - 1]?.last,
indexOfLastItemInPreviousRow + 1 < numberOfItems,
let maxYForPreviousRow = maxYForItemsRow(atIndex: rowIndex - 1)
{
// There's a previous row of items, so we'll use the max Y of that row as the starting place
// for the current row of items.
startingItemIndex = indexOfLastItemInPreviousRow + 1
currentY = maxYForPreviousRow + metrics.verticalSpacing
} else if (headerModel == nil && rowIndex == 0) || (headerModel != nil && rowIndex == 1) {
// Our starting row doesn't exist yet, so we'll lay out our first row of items.
startingItemIndex = 0
currentY = (headerModel?.originInSection.y ?? metrics.sectionInsets.top) +
(headerModel?.size.height ?? 0)
} else {
// Our starting row is after the last row of items, so we'll skip item layout.
startingItemIndex = numberOfItems
if
let lastRowIndex = indexOfLastItemsRow(),
rowIndex > lastRowIndex,
let maxYOfLastRowOfItems = maxYForItemsRow(atIndex: lastRowIndex)
{
currentY = maxYOfLastRowOfItems
} else {
currentY = (headerModel?.originInSection.y ?? metrics.sectionInsets.top) +
(headerModel?.size.height ?? 0)
}
}
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
// and Swift retain / release calls.
let itemModelsPointer = UnsafeMutableRawPointer(mutating: &itemModels)
let directlyMutableItemModels = itemModelsPointer.assumingMemoryBound(to: ItemModel.self)
var indexInCurrentRow = 0
for itemIndex in startingItemIndex..<numberOfItems {
// Create item / row index mappings
itemIndicesForRowIndices[rowIndex] = itemIndicesForRowIndices[rowIndex] ?? []
itemIndicesForRowIndices[rowIndex]?.append(itemIndex)
rowIndicesForItemIndices[itemIndex] = rowIndex
let itemModel = itemModels[itemIndex]
if itemIndex == 0 {
// Apply top item inset now that we're laying out items
currentY += metrics.itemInsets.top
}
let currentLeadingMargin: CGFloat
let availableWidthForItems: CGFloat
if itemModel.sizeMode.widthMode == .fullWidth(respectsHorizontalInsets: false) {
currentLeadingMargin = metrics.sectionInsets.left
availableWidthForItems = metrics.width
} else {
currentLeadingMargin = metrics.sectionInsets.left + metrics.itemInsets.left
availableWidthForItems = metrics.width - metrics.itemInsets.left - metrics.itemInsets.right
}
let totalSpacing = metrics.horizontalSpacing * (itemModel.sizeMode.widthMode.widthDivisor - 1)
let itemWidth = round(
(availableWidthForItems - totalSpacing) / itemModel.sizeMode.widthMode.widthDivisor)
let itemX = CGFloat(indexInCurrentRow) *
itemWidth + CGFloat(indexInCurrentRow) *
metrics.horizontalSpacing + currentLeadingMargin
let itemY = currentY
directlyMutableItemModels[itemIndex].originInSection = CGPoint(x: itemX, y: itemY)
directlyMutableItemModels[itemIndex].size.width = itemWidth
if
(indexInCurrentRow == Int(itemModel.sizeMode.widthMode.widthDivisor) - 1) ||
(itemIndex == numberOfItems - 1) ||
(itemIndex < numberOfItems - 1 && itemModels[itemIndex + 1].sizeMode.widthMode != itemModel.sizeMode.widthMode)
{
// We've reached the end of the current row, or there are no more items to lay out, or we're
// about to lay out an item with a different width mode. In all cases, we're done laying out
// the current row of items.
let heightOfTallestItemInCurrentRow = updateHeightsForItemsInRow(at: rowIndex)
currentY += heightOfTallestItemInCurrentRow
indexInCurrentRow = 0
// If there are more items to layout, add vertical spacing and increment the row index
if itemIndex < numberOfItems - 1 {
currentY += metrics.verticalSpacing
rowIndex += 1
}
} else {
// We're still adding to the current row
indexInCurrentRow += 1
}
}
if numberOfItems > 0 {
// Apply bottom item inset now that we're done laying out items
currentY += metrics.itemInsets.bottom
}
// Footer frame calculations
if let existingFooterModel = footerModel {
rowIndex += 1
footerModel?.originInSection = CGPoint(x: metrics.sectionInsets.left, y: currentY)
footerModel?.size.width = metrics.width
updateFooterHeight(withMetricsFrom: existingFooterModel)
}
numberOfRows = rowIndex + 1
// Final height calculation
calculatedHeight = currentY + (footerModel?.size.height ?? 0) + metrics.sectionInsets.bottom
// The background frame is calculated just-in-time, since its value doesn't affect the layout.
// Create a row offset tracker now that we know how many rows we have
rowOffsetTracker = RowOffsetTracker(numberOfRows: numberOfRows)
// Mark the layout as clean / no longer invalid
indexOfFirstInvalidatedRow = nil
}
private mutating func updateHeightsForItemsInRow(at rowIndex: Int) -> CGFloat {
guard let indicesForItemsInRow = itemIndicesForRowIndices[rowIndex] else {
assertionFailure("Expected item indices for row \(rowIndex).")
return 0
}
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
// and Swift retain / release calls.
let itemModelsPointer = UnsafeMutableRawPointer(mutating: &itemModels)
let directlyMutableItemModels = itemModelsPointer.assumingMemoryBound(to: ItemModel.self)
var heightOfTallestItem = CGFloat(0)
var stretchToTallestItemInRowItemIndices = Set<Int>()
for itemIndex in indicesForItemsInRow {
let preferredHeight = itemModels[itemIndex].preferredHeight
let height = itemModels[itemIndex].size.height
directlyMutableItemModels[itemIndex].size.height = preferredHeight ?? height
// Handle stretch to tallest item in row height mode for current row
if itemModels[itemIndex].sizeMode.heightMode == .dynamicAndStretchToTallestItemInRow {
stretchToTallestItemInRowItemIndices.insert(itemIndex)
}
heightOfTallestItem = max(heightOfTallestItem, itemModels[itemIndex].size.height)
}
for stretchToTallestItemInRowItemIndex in stretchToTallestItemInRowItemIndices{
directlyMutableItemModels[stretchToTallestItemInRowItemIndex].size.height = heightOfTallestItem
}
itemRowHeightsForRowIndices[rowIndex] = heightOfTallestItem
return heightOfTallestItem
}
@discardableResult
private mutating func updateHeaderHeight(withMetricsFrom headerModel: HeaderModel) -> CGFloat {
let height = headerModel.preferredHeight ?? headerModel.size.height
self.headerModel?.size.height = height
return height
}
@discardableResult
private mutating func updateFooterHeight(withMetricsFrom footerModel: FooterModel) -> CGFloat {
let height = footerModel.preferredHeight ?? footerModel.size.height
self.footerModel?.size.height = height
return height
}
}
@@ -0,0 +1,34 @@
// Created by bryankeller on 2/25/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
/// Represents a collection view update with more type-expressivity than
/// `UICollectionViewUpdateItem`.
enum CollectionViewUpdate<SectionModel, ItemModel> {
case sectionReload(sectionIndex: Int, newSection: SectionModel)
case itemReload(itemIndexPath: IndexPath, newItem: ItemModel)
case sectionDelete(sectionIndex: Int)
case itemDelete(itemIndexPath: IndexPath)
case sectionInsert(sectionIndex: Int, newSection: SectionModel)
case itemInsert(itemIndexPath: IndexPath, newItem: ItemModel)
case sectionMove(initialSectionIndex: Int, finalSectionIndex: Int)
case itemMove(initialItemIndexPath: IndexPath, finalItemIndexPath: IndexPath)
}
@@ -0,0 +1,46 @@
// Created by bryankeller on 8/13/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// Represents the location of an item in a section.
///
/// Initializing a `ElementLocation` is measurably faster than initializing an `IndexPath`.
/// On an iPhone X, compiled with -Os optimizations, it's about 35x faster to initialize this struct
/// compared to an `IndexPath`.
struct ElementLocation: Hashable {
// MARK: Lifecycle
init(elementIndex: Int, sectionIndex: Int) {
self.elementIndex = elementIndex
self.sectionIndex = sectionIndex
}
init(indexPath: IndexPath) {
elementIndex = indexPath.item
sectionIndex = indexPath.section
}
// MARK: Internal
let elementIndex: Int
let sectionIndex: Int
var indexPath: IndexPath {
return IndexPath(item: elementIndex, section: sectionIndex)
}
}
@@ -0,0 +1,134 @@
// Created by bryankeller on 8/17/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
// MARK: - ElementLocationFramePairs
/// Stores pairs of `ElementLocationFramePair`s in an efficient way for appending to and
/// iterating over.
///
/// The main reason this exists (and why its implementation uses a singly-linked-list) is so that,
/// as we find the section and item index and frame for an element in a visible rect, we can append
/// it to this data structure without copy-on-write performance issues, or array buffer resizing
/// issues. (The cost of appending to an array is much more expensive due to copy-on-write and the
/// backing buffer needing to be resized).
struct ElementLocationFramePairs {
// MARK: Lifecycle
init() { }
init(elementLocationFramePair: ElementLocationFramePair) {
append(elementLocationFramePair)
}
// MARK: Internal
mutating func append(_ elementLocationFramePair: ElementLocationFramePair) {
if first == nil {
first = elementLocationFramePair
} else {
last.next = elementLocationFramePair
}
last = elementLocationFramePair
}
// MARK: Fileprivate
fileprivate var first: ElementLocationFramePair?
// MARK: Private
private var last: ElementLocationFramePair!
}
// MARK: Sequence
extension ElementLocationFramePairs: Sequence {
func makeIterator() -> ElementLocationFramePairsIterator {
return ElementLocationFramePairsIterator(self)
}
}
// MARK: - ElementLocationFramePairsIterator
/// Used for iterating through `ElementLocationFramePairs` instances
struct ElementLocationFramePairsIterator: IteratorProtocol {
typealias Element = ElementLocationFramePair
// MARK: Lifecycle
init(_ elementLocationFramePairs: ElementLocationFramePairs) {
self.elementLocationFramePairs = elementLocationFramePairs
}
// MARK: Internal
mutating func next() -> ElementLocationFramePair? {
if lastReturnedElement == nil {
lastReturnedElement = elementLocationFramePairs.first
} else {
lastReturnedElement = lastReturnedElement?.next
}
return lastReturnedElement
}
// MARK: Private
private let elementLocationFramePairs: ElementLocationFramePairs
private var lastReturnedElement: ElementLocationFramePair?
}
// MARK: - ElementLocationFramePair
/// Encapsulates a `ElementLocation` and a `CGRect` frame for an element.
final class ElementLocationFramePair {
// MARK: Lifecycle
init(elementLocation: ElementLocation, frame: CGRect) {
self.elementLocation = elementLocation
self.frame = frame
}
// MARK: Internal
let elementLocation: ElementLocation
let frame: CGRect
// MARK: Fileprivate
fileprivate var next: ElementLocationFramePair?
}
// MARK: Equatable
extension ElementLocationFramePair: Equatable {
static func == (lhs: ElementLocationFramePair, rhs: ElementLocationFramePair) -> Bool {
return lhs.elementLocation == rhs.elementLocation && lhs.frame == rhs.frame
}
}
@@ -0,0 +1,31 @@
// Created by bryankeller on 10/15/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
extension MagazineLayoutItemWidthMode {
/// Returns the divisor for a given item width mode.
///
/// When divided into the width of the collection view, the result equals the width of the item
/// before taking into account horizontal insets.
var widthDivisor: CGFloat {
switch self {
case .fullWidth: return 1
case let .fractionalWidth(divisor): return CGFloat(divisor)
}
}
}
@@ -0,0 +1,107 @@
// Created by bryankeller on 10/26/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
/// Encapsulates all layout-affecting metrics relating to a section
struct MagazineLayoutSectionMetrics: Equatable {
// MARK: Lifecycle
init(
forSectionAtIndex sectionIndex: Int,
in collectionView: UICollectionView,
layout: UICollectionViewLayout,
delegate: UICollectionViewDelegateMagazineLayout)
{
collectionViewWidth = collectionView.bounds.width
if #available(iOS 11.0, *) {
collectionViewContentInset = collectionView.adjustedContentInset
} else {
collectionViewContentInset = collectionView.contentInset
}
verticalSpacing = delegate.collectionView(
collectionView,
layout: layout,
verticalSpacingForElementsInSectionAtIndex: sectionIndex)
horizontalSpacing = delegate.collectionView(
collectionView,
layout: layout,
horizontalSpacingForItemsInSectionAtIndex: sectionIndex)
sectionInsets = delegate.collectionView(
collectionView,
layout: layout,
insetsForSectionAtIndex: sectionIndex)
itemInsets = delegate.collectionView(
collectionView,
layout: layout,
insetsForItemsInSectionAtIndex: sectionIndex)
}
private init(
collectionViewWidth: CGFloat,
collectionViewContentInset: UIEdgeInsets,
verticalSpacing: CGFloat,
horizontalSpacing: CGFloat,
sectionInsets: UIEdgeInsets,
itemInsets: UIEdgeInsets)
{
self.collectionViewWidth = collectionViewWidth
self.collectionViewContentInset = collectionViewContentInset
self.verticalSpacing = verticalSpacing
self.horizontalSpacing = horizontalSpacing
self.sectionInsets = sectionInsets
self.itemInsets = itemInsets
}
// MARK: Internal
var width: CGFloat {
return collectionViewWidth -
collectionViewContentInset.left -
collectionViewContentInset.right -
sectionInsets.left -
sectionInsets.right
}
var verticalSpacing: CGFloat
var horizontalSpacing: CGFloat
var sectionInsets: UIEdgeInsets
var itemInsets: UIEdgeInsets
static func defaultSectionMetrics(
forCollectionViewWidth width: CGFloat)
-> MagazineLayoutSectionMetrics
{
return MagazineLayoutSectionMetrics(
collectionViewWidth: width,
collectionViewContentInset: .zero,
verticalSpacing: MagazineLayout.Default.VerticalSpacing,
horizontalSpacing: MagazineLayout.Default.HorizontalSpacing,
sectionInsets: MagazineLayout.Default.SectionInsets,
itemInsets: MagazineLayout.Default.ItemInsets)
}
// MARK: Private
private let collectionViewWidth: CGFloat
private let collectionViewContentInset: UIEdgeInsets
}
@@ -0,0 +1,79 @@
// Created by bryankeller on 5/23/19.
// Copyright © 2019 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
/// Tracks offsets for rows using a Segment Tree for O(log n) lookups and updates.
struct RowOffsetTracker {
// MARK: Lifecycle
init(numberOfRows: Int) {
self.numberOfRows = numberOfRows
rowOffsets = Array(repeating: 0, count: 2 * numberOfRows)
}
// MARK: Internal
mutating func addOffset(_ offset: CGFloat, forRowsStartingAt rowIndex: Int) {
var rowIndex = rowIndex + numberOfRows
// Accessing this array using an unsafe, untyped (raw) pointer avoids expensive copy-on-writes
// and Swift retain / release calls.
let rowOffsetsPointer = UnsafeMutableRawPointer(mutating: &rowOffsets)
let directlyMutableRowOffsets = rowOffsetsPointer.assumingMemoryBound(to: CGFloat.self)
directlyMutableRowOffsets[rowIndex] = rowOffsets[rowIndex] + offset
while rowIndex > 1 {
rowIndex /= 2
let leftChild = rowOffsets[2 * rowIndex]
let rightChild = rowOffsets[(2 * rowIndex) + 1]
directlyMutableRowOffsets[rowIndex] = leftChild + rightChild
}
}
func offsetForRow(at rowIndex: Int) -> CGFloat {
var lowerBound = numberOfRows
var upperBound = rowIndex + numberOfRows + 1
var offset = CGFloat(0)
while lowerBound < upperBound {
if lowerBound % 2 != 0 {
offset += rowOffsets[lowerBound]
lowerBound += 1
}
if upperBound % 2 != 0 {
upperBound -= 1
offset += rowOffsets[upperBound]
}
lowerBound /= 2
upperBound /= 2
}
return offset
}
// MARK: Private
private let numberOfRows: Int
private var rowOffsets: [CGFloat]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
// Created by bryankeller on 7/24/17.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
/// Encapsulates the vertical fitting priority for an element laid out by `MagazineLayout`.
///
/// Used by `UICollectionViewCell` and `UICollectionReusableView` subclasses to determine which
/// vertical fitting priority to pass into
/// `systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority)` via
/// `preferredLayoutAttributesFitting(_:)`.
public final class MagazineLayoutCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
/// `MagazineLayout` supports self-sizing and static-sizing in the vertical direction. The value
/// of this property will change the layout priority used for sizing.
public var shouldVerticallySelfSize = true
override public func copy(with zone: NSZone? = nil) -> Any {
let copy = super.copy(with: zone) as! MagazineLayoutCollectionViewLayoutAttributes
copy.shouldVerticallySelfSize = shouldVerticallySelfSize
return copy
}
override public func isEqual(_ object: Any?) -> Bool {
return super.isEqual(object) &&
shouldVerticallySelfSize == (object as? MagazineLayoutCollectionViewLayoutAttributes)?.shouldVerticallySelfSize
}
}
@@ -0,0 +1,27 @@
// Created by bryankeller on 9/24/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
/// `MagazineLayout`'s invalidation context type.
///
/// Used to indicate that collection view properties and/or delegate layout metrics changed.
public final class MagazineLayoutInvalidationContext: UICollectionViewLayoutInvalidationContext {
/// Indicates whether to recompute the positions and sizes of elements based on the current
/// collection view and delegate layout metrics.
public var invalidateLayoutMetrics = true
}
@@ -0,0 +1,40 @@
// Created by bryankeller on 10/18/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
extension MagazineLayout {
/// Constants for layout sizing and spacing defaults.
public enum Default {
public static let ItemSizeMode = MagazineLayoutItemSizeMode(
widthMode: .fullWidth(respectsHorizontalInsets: true),
heightMode: MagazineLayoutItemHeightMode.static(height: ItemHeight))
public static let HeaderVisibilityMode = MagazineLayoutHeaderVisibilityMode.hidden
public static let FooterVisibilityMode = MagazineLayoutFooterVisibilityMode.hidden
public static let BackgroundVisibilityMode = MagazineLayoutBackgroundVisibilityMode.hidden
public static let ItemHeight: CGFloat = 150
public static let HeaderHeight: CGFloat = 44
public static let FooterHeight: CGFloat = 44
public static let VerticalSpacing: CGFloat = 0
public static let HorizontalSpacing: CGFloat = 0
public static let SectionInsets: UIEdgeInsets = .zero
public static let ItemInsets: UIEdgeInsets = .zero
}
}
@@ -0,0 +1,27 @@
// Created by bryankeller on 10/18/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
extension MagazineLayout {
/// Constants for supported supplementary view element kinds.
public enum SupplementaryViewKind {
public static let sectionHeader = "MagazineLayoutSupplementaryViewKindSectionHeader"
public static let sectionFooter = "MagazineLayoutSupplementaryViewKindSectionFooter"
public static let sectionBackground = "MagazineLayoutSupplementaryViewKindSectionBackground"
}
}
@@ -0,0 +1,27 @@
// Created by bryankeller on 10/15/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// Represents the visibility mode for a background.
public enum MagazineLayoutBackgroundVisibilityMode {
/// This visiblity mode will cause the background to be displayed behind the items and headers in
/// its respective section.
case visible
/// This visibility mode will cause the background to not be visibile behind the items and headers
/// in its respective section.
case hidden
}
@@ -0,0 +1,78 @@
// Created by Roman Laitarenko on 2/4/19.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
// MARK: - MagazineLayoutFooterVisibilityMode
/// Represents the visibility mode for a footer.
public enum MagazineLayoutFooterVisibilityMode {
/// This visibility mode will cause the footer to be displayed using the specified height mode in
/// its respective section. If `pinToVisibleBounds` is true, the footer will pin to the visible
/// bounds of the collection view while its containing section is visible.
case visible(heightMode: MagazineLayoutFooterHeightMode, pinToVisibleBounds: Bool)
/// This visibility mode will cause the footer to not be visibile in its respective section.
case hidden
/// This visibility mode will cause the footer to be displayed using the specified height mode in
/// its respective section.
public static func visible(
heightMode: MagazineLayoutFooterHeightMode)
-> MagazineLayoutFooterVisibilityMode
{
return .visible(heightMode: heightMode, pinToVisibleBounds: false)
}
}
// MARK: - MagazineLayoutFooterHeightMode
/// Represents the vertical sizing mode for a footer.
public enum MagazineLayoutFooterHeightMode {
/// This height mode will force the footer to be displayed with a height equal to `height`.
///
/// To properly support multiline labels, dynamic type, and other technologies that could affect
/// the height of your footers dynamically, consider using the `dynamic` height mode.
case `static`(height: CGFloat)
/// This height mode will cause the footer to self-size in the vertical direction.
///
/// In practice, self-sizing in the vertical direction means that the footer will get its height
/// from the Auto Layout engine. Use this height mode for footers whose height is not known
/// upfront. For example, if you support multiline labels or dynamic type, your height is likely
/// not known until the Auto Layout engine resolves the layout at runtime.
case dynamic
}
// MARK: Equatable
extension MagazineLayoutFooterHeightMode: Equatable {
public static func == (
lhs: MagazineLayoutFooterHeightMode,
rhs: MagazineLayoutFooterHeightMode)
-> Bool
{
switch (lhs, rhs) {
case (.static(let l), .static(let r)): return l == r
case (.dynamic, .dynamic): return true
default: return false
}
}
}
@@ -0,0 +1,79 @@
// Created by bryankeller on 10/15/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
// MARK: - MagazineLayoutHeaderVisibilityMode
/// Represents the visibility mode for a header.
public enum MagazineLayoutHeaderVisibilityMode {
/// This visibility mode will cause the header to be displayed using the specified height mode in
/// its respective section. If `pinToVisibleBounds` is true, the header will pin to the visible
/// bounds of the collection view while its containing section is visible.
case visible(heightMode: MagazineLayoutHeaderHeightMode, pinToVisibleBounds: Bool)
/// This visibility mode will cause the header to not be visibile in its respective section.
case hidden
/// This visibility mode will cause the header to be displayed using the specified height mode in
/// its respective section.
public static func visible(
heightMode: MagazineLayoutHeaderHeightMode)
-> MagazineLayoutHeaderVisibilityMode
{
return .visible(heightMode: heightMode, pinToVisibleBounds: false)
}
}
// MARK: - MagazineLayoutHeaderHeightMode
/// Represents the vertical sizing mode for a header.
public enum MagazineLayoutHeaderHeightMode {
/// This height mode will force the header to be displayed with a height equal to `height`.
///
/// To properly support multiline labels, dynamic type, and other technologies that could affect
/// the height of your headers dynamically, consider using the `dynamic` height mode.
case `static`(height: CGFloat)
/// This height mode will cause the header to self-size in the vertical direction.
///
/// In practice, self-sizing in the vertical direction means that the header will get its height
/// from the Auto Layout engine. Use this height mode for headers whose height is not known
/// upfront. For example, if you support multiline labels or dynamic type, your height is likely
/// not known until the Auto Layout engine resolves the layout at runtime.
case dynamic
}
// MARK: Equatable
extension MagazineLayoutHeaderHeightMode: Equatable {
public static func == (
lhs: MagazineLayoutHeaderHeightMode,
rhs: MagazineLayoutHeaderHeightMode)
-> Bool
{
switch (lhs, rhs) {
case (.static(let l), .static(let r)): return l == r
case (.dynamic, .dynamic): return true
default: return false
}
}
}
@@ -0,0 +1,164 @@
// Created by bryankeller on 10/15/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import CoreGraphics
// MARK: - MagazineLayoutItemSizeMode
/// Represents the horizontal and vertical sizing mode for an item.
public struct MagazineLayoutItemSizeMode {
// MARK: Lifecycle
public init(widthMode: MagazineLayoutItemWidthMode, heightMode: MagazineLayoutItemHeightMode) {
self.widthMode = widthMode
self.heightMode = heightMode
}
// MARK: Public
/// The width mode for the item.
public let widthMode: MagazineLayoutItemWidthMode
/// The height mode for the item.
public let heightMode: MagazineLayoutItemHeightMode
}
// MARK: - MagazineLayoutItemWidthMode
/// Represents the horizontal sizing mode for an item.
///
/// Consecutive items with the same width mode will display on the same row until there is no more
/// room.
public enum MagazineLayoutItemWidthMode {
/// Full width items will fill the available width in a section.
///
/// Use this width mode to create lists of items.
/// `respectsHorizontalInsets` specifies whether the item should be edge-to-edge in a section, or
/// if it should be inset by the item insets specified for a section. `respectsHorizontalInsets`
/// does not take into account section insets or the collection view's content inset.
case fullWidth(respectsHorizontalInsets: Bool)
/// Fractional width items will take up `1/divisor` of the available width for a given row of
/// items.
///
/// Use this width mode to create grids of items. Consider using `halfWidth`, `thirdWidth`,
/// `fourthWidth`, or `fifthWidth`, which are equivalent to using `fractionalWidth` with a
/// `divisor` of `2`, `3`, `4`, or `5`, respectively.
///
/// Fractional width items respect `contentInset.left` and `contentInset.right`, and are affected
/// by the horizontal spacing specified for the section in which they're contained. On iOS 11 and
/// higher, they will also take the safe area insets into account if the collection view's
/// `contentInsetAdjustmentBehavior` property is set to a value that respects the safe area.
///
/// - Warning: `divisor` must be greater than `0`. Specifying `0` as the `divisor` is a programmer
/// error and **will result in a runtime crash**.
case fractionalWidth(divisor: UInt)
/// Half width items will take up `1/2` of the available width for a given row of items.
public static var halfWidth: MagazineLayoutItemWidthMode {
return .fractionalWidth(divisor: 2)
}
/// Third width items will take up `1/3` of the available width for a given row of items.
public static var thirdWidth: MagazineLayoutItemWidthMode {
return .fractionalWidth(divisor: 3)
}
/// Fourth width items will take up `1/4` of the available width for a given row of items.
public static var fourthWidth: MagazineLayoutItemWidthMode {
return .fractionalWidth(divisor: 4)
}
/// Fifth width items will take up `1/5` of the available width for a given row of items.
public static var fifthWidth: MagazineLayoutItemWidthMode {
return .fractionalWidth(divisor: 5)
}
}
// MARK: Equatable
extension MagazineLayoutItemWidthMode: Equatable {
public static func == (
lhs: MagazineLayoutItemWidthMode,
rhs: MagazineLayoutItemWidthMode)
-> Bool
{
switch (lhs, rhs) {
case (.fullWidth(let l), .fullWidth(let r)): return l == r
case (.fractionalWidth(let l), .fractionalWidth(let r)): return l == r
default: return false
}
}
}
// MARK: - MagazineLayoutItemHeightMode
/// Represents the vertical sizing mode for an item.
///
/// `MagazineLayout` supports vertically self-sizing and statically sized items. Since height modes
/// are specified for each item, you can mix vertically self-sizing and statically sized items in
/// the same sections, and even in the same rows.
public enum MagazineLayoutItemHeightMode {
/// This height mode mode will cause the item to be displayed with a height equal to `height`.
///
/// To properly support multiline labels, dynamic type, and other technologies that could affect
/// the height of your items dynamically, consider using one of the dynamic height modes.
case `static`(height: CGFloat)
/// This height mode will cause the item to self-size in the vertical direction.
///
/// In practice, self-sizing in the vertical direction means that the item will get its height
/// from the Auto Layout engine. Use this height mode for items whose height is not known upfront.
/// For example, if you support multiline labels or dynamic type, your height is likely not known
/// until the Auto Layout engine resolves the layout at runtime.
case dynamic
/// This height mode will cause the item to self-size in the vertical direction, then resize to
/// match the height of the tallest item in the same row of items.
///
/// If the item _is_ the tallest item in the row (after being self-sized), then it will stay
/// at its self-sized height until it's no longer the tallest item in the row.
///
/// Note that items with this height mode will resize to match the height of the tallest item in
/// the same row of items, even if the tallest item has a `static` height mode.
case dynamicAndStretchToTallestItemInRow
}
// MARK: Equatable
extension MagazineLayoutItemHeightMode: Equatable {
public static func == (
lhs: MagazineLayoutItemHeightMode,
rhs: MagazineLayoutItemHeightMode)
-> Bool
{
switch (lhs, rhs) {
case (.static(let l), .static(let r)): return l == r
case (.dynamic, .dynamic): return true
case (.dynamicAndStretchToTallestItemInRow, .dynamicAndStretchToTallestItemInRow): return true
default: return false
}
}
}
@@ -0,0 +1,138 @@
// Created by bryankeller on 7/17/17.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
public protocol UICollectionViewDelegateMagazineLayout: UICollectionViewDelegate {
/// Asks the delegate for the size mode of the specified item.
///
/// - Parameters:
/// - collectionView: The collection view using the layout.
/// - collectionViewLayout: The layout requesting the information.
/// - indexPath: The index path of the item.
///
/// - Returns: The size mode of the specified item.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeModeForItemAt indexPath: IndexPath)
-> MagazineLayoutItemSizeMode
/// Asks the delegate for the visibility mode of the header in the specified section.
///
/// - Parameters:
/// - collectionView: The collection view using the layout.
/// - collectionViewLayout: The layout requesting the information.
/// - index: The index of the section containing the header.
///
/// - Returns: The visibility mode of the header in the specified section.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
visibilityModeForHeaderInSectionAtIndex index: Int)
-> MagazineLayoutHeaderVisibilityMode
/// Asks the delegate for the visibility mode of the footer in the specified section.
///
/// - Parameters:
/// - collectionView: The collection view using the layout.
/// - collectionViewLayout: The layout requesting the information.
/// - index: The index of the section containing the footer.
///
/// - Returns: The visibility mode of the footer in the specified section.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
visibilityModeForFooterInSectionAtIndex index: Int)
-> MagazineLayoutFooterVisibilityMode
/// Asks the delegate for the visibility mode of the background in the specified section.
///
/// - Parameters:
/// - collectionView: The collection view using the layout.
/// - collectionViewLayout: The layout requesting the information.
/// - index: The index of the section containing the background.
///
/// - Returns: The visibility mode of the background in the specified section.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
visibilityModeForBackgroundInSectionAtIndex index: Int)
-> MagazineLayoutBackgroundVisibilityMode
/// Asks the delegate for the horizontal spacing for items in the specified section.
///
/// - Parameters:
/// - collectionView: The collection view using the layout.
/// - collectionViewLayout: The layout requesting the information.
/// - index: The index of the section whose horizontal item spacing is needed.
///
/// - Returns: The horizontal spacing for items in the specified section.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
horizontalSpacingForItemsInSectionAtIndex index: Int)
-> CGFloat
/// Asks the delegate for the vertical spacing for items in the specified section.
///
/// - Parameters:
/// - collectionView: The collection view using the layout.
/// - collectionViewLayout: The layout requesting the information.
/// - index: The index of the section whose vertical item spacing is needed.
///
/// - Returns: The vertical spacing for items in the specified section.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
verticalSpacingForElementsInSectionAtIndex index: Int)
-> CGFloat
/// Asks the delegate for the amount by which to inset elements in the specified section.
///
/// Section insets are relative to the content's bounds, which is impacted by the collection
/// view's content inset.
///
/// - Parameters:
/// - collectionView: The collection view using the layout.
/// - collectionViewLayout: The layout requesting the information.
/// - index: The index of the section whose element insets are needed.
///
/// - Returns: The amount by which to inset elements in the specified section.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetsForSectionAtIndex index: Int)
-> UIEdgeInsets
/// Asks the delegate for the amount by which to inset items in the specified section.
///
/// Item insets are relative to the section's bounds, which is impacted by the section's insets
/// and, transitively, the collection view's content inset.
///
/// - Parameters:
/// - collectionView: The collection view using the layout.
/// - collectionViewLayout: The layout requesting the information.
/// - index: The index of the section whose item insets are needed.
///
/// - Returns: The amount by which to inset items in the specified section.
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetsForItemsInSectionAtIndex index: Int)
-> UIEdgeInsets
}
@@ -0,0 +1,61 @@
// Created by bryankeller on 11/29/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
/// A collection reusable view that coordinates with `MagazineLayoutCollectionViewLayoutAttributes`
/// to determine how to size itself: with self-sizing, or without self-sizing. Use this class
/// (or subclasses) for displaying header and background supplementary views with `MagazineLayout`.
///
/// Note that this class is very similar to `MagazineLayoutCollectionViewCell`.
///
/// `UIKit` invokes `preferredLayoutAttributesFitting(_:)` with an initial set of layout attributes,
/// giving this reusable view subclass a chance to modify the `size` property of the attributes
/// based on whether or not we want to self-size.
///
/// Subclassing and/or adding additional protocol conformances is encouraged, although modifying
/// the behavior of `preferredLayoutAttributesFitting(_:)` is not recommended.
///
/// This class exists because `MagazineLayout` supports self-sizing supplementary views in just the
/// vertical dimension - a use case that `UICollectionReusableView` does not support out-of-the-box.
open class MagazineLayoutCollectionReusableView: UICollectionReusableView {
override open func preferredLayoutAttributesFitting(
_ layoutAttributes: UICollectionViewLayoutAttributes)
-> UICollectionViewLayoutAttributes
{
guard let attributes = layoutAttributes as? MagazineLayoutCollectionViewLayoutAttributes else {
assertionFailure("`layoutAttributes` must be an instance of `MagazineLayoutCollectionViewLayoutAttributes`")
return super.preferredLayoutAttributesFitting(layoutAttributes)
}
let size: CGSize
if attributes.shouldVerticallySelfSize {
// Self-sizing is required in the vertical dimension.
size = super.systemLayoutSizeFitting(
layoutAttributes.size,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
} else {
// No self-sizing is required; respect whatever size the layout determined.
size = layoutAttributes.size
}
layoutAttributes.size = size
return layoutAttributes
}
}
@@ -0,0 +1,88 @@
// Created by bryankeller on 11/29/18.
// Copyright © 2018 Airbnb, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import UIKit
/// A cell that coordinates with `MagazineLayoutCollectionViewLayoutAttributes` to determine how to
/// size itself: with self-sizing, or without self-sizing. Use this class (or subclasses) for
/// displaying cells with `MagazineLayout`.
///
/// Note that this class is very similar to `MagazineLayoutCollectionReusableView`.
///
/// `UIKit` invokes `preferredLayoutAttributesFitting(_:)` with an initial set of layout attributes,
/// giving this cell subclass a chance to modify the `size` property of the attributes based on
/// whether or not we want to self-size.
///
/// Subclassing and/or adding additional protocol conformances is encouraged, although modifying
/// the behavior of `preferredLayoutAttributesFitting(_:)` is not recommended.
///
/// This class exists because `MagazineLayout` supports self-sizing in just the vertical dimension -
/// a use case that `UICollectionViewCell` does not support out-of-the-box.
///
/// As of iOS 12, `UICollectionReusableView` is tightly coupled with `UICollectionViewFlowLayout`'s
/// private `_estimatesSizes` property, which is used to determine how to size cells displayed in a
/// `UICollectionViewFlowLayout`. If `_estimatesSizes` is `true`, then `UICollectionReusableView`
/// will self-size in both the horizontal and vertical dimensions. If it is `false`, no self-sizing
/// will occur. In short, `UICollectionReusableView` is only optimized to work correctly with
/// Apple's own layout.
open class MagazineLayoutCollectionViewCell: UICollectionViewCell {
override open func preferredLayoutAttributesFitting(
_ layoutAttributes: UICollectionViewLayoutAttributes)
-> UICollectionViewLayoutAttributes
{
guard let attributes = layoutAttributes as? MagazineLayoutCollectionViewLayoutAttributes else {
assertionFailure("`layoutAttributes` must be an instance of `MagazineLayoutCollectionViewLayoutAttributes`")
return super.preferredLayoutAttributesFitting(layoutAttributes)
}
// In some cases, `contentView`'s required width and height constraints
// (created from its auto-resizing mask) will not have the correct constants before invoking
// `systemLayoutSizeFitting(...)`, causing the cell to size incorrectly. This seems to be a
// UIKit bug.
// https://openradar.appspot.com/radar?id=5025850143539200
// The issue seems most common when the collection view's bounds change (on rotation).
// We correct for this by updating `contentView.bounds`, which updates the constants used by the
// width and height constraints created by the `contentView`'s auto-resizing mask.
if contentView.bounds.width != layoutAttributes.size.width {
contentView.bounds.size.width = layoutAttributes.size.width
}
if
!attributes.shouldVerticallySelfSize &&
contentView.bounds.height != layoutAttributes.size.height
{
contentView.bounds.size.height = layoutAttributes.size.height
}
let size: CGSize
if attributes.shouldVerticallySelfSize {
// Self-sizing is required in the vertical dimension.
size = super.systemLayoutSizeFitting(
layoutAttributes.size,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
} else {
// No self-sizing is required; respect whatever size the layout determined.
size = layoutAttributes.size
}
layoutAttributes.size = size
return layoutAttributes
}
}
+269
View File
@@ -0,0 +1,269 @@
# MagazineLayout
A collection view layout capable of laying out views in vertically scrolling grids and lists.
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Version](https://img.shields.io/cocoapods/v/MagazineLayout.svg)](https://cocoapods.org/pods/MagazineLayout)
[![License](https://img.shields.io/cocoapods/l/MagazineLayout.svg)](https://cocoapods.org/pods/MagazineLayout)
[![Platform](https://img.shields.io/cocoapods/p/MagazineLayout.svg)](https://cocoapods.org/pods/MagazineLayout)
[![Build Status](https://travis-ci.com/airbnb/MagazineLayout.svg?token=yiLD1pPhUW32MDs2wsVE&branch=master)](https://travis-ci.com/airbnb/MagazineLayout)
## Introduction
`MagazineLayout` is a `UICollectionViewLayout` subclass for laying out vertically scrolling grids and lists of items. Compared to `UICollectionViewFlowLayout`, `MagazineLayout` supports many additional features:
- Item widths based on a fraction of the total available width
- Full width for a list layout (similar to `UITableView`)
- Half-width, third-width, etc. for a grid layout
- Self-sizing in just the vertical dimension
- Per-item self-sizing preferences (self-size and statically-size items anywhere in your collection view)
- Self-sizing headers and footers
- Hiding or showing headers and footers on a per-section basis
- Pinned (sticky) headers and footers
- Section backgrounds that can be hidden / visible on a per-section basis
Other features:
- Specifying horizontal item spacing on a per-section basis
- Specifying vertical row spacing on a per-section basis
- Specifying section insets on a per-section basis
- Specifying item insets on a per-section basis
These capabilities have allowed us to build a wide variety of screens in the Airbnb app, many of which are among our highest-traffic screens. Here are just a few examples of screens laid out using `MagazineLayout`:
| Homes Search | Experiences Search | Wish List | Home |
| --- | --- | --- | --- |
| ![Homes Search](Docs/Images/SearchHomes.png) | ![Experiences Search](Docs/Images/SearchExperiences.png) | ![Wish list](Docs/Images/WishList.png) | ![Home](Docs/Images/HomePDP.png) |
| Plus Home | Plus Home Tour | Trips | Trip Detail |
| --- | --- | --- | --- |
|![Plus Home](Docs/Images/PlusHomePDP.png) | ![Plus Home Tour](Docs/Images/PlusHomeTour.png) | ![Trips](Docs/Images/Trips.png) | ![Trip Detail](Docs/Images/TripDetail.png) |
## Table of Contents
- [Example App](#example-app)
- [Using the Example App](#using-the-example-app)
- [Adding a new item](#adding-a-new-item)
- [Deleting an item](#deleting-an-item)
- [Getting Started](#getting-started)
- [Requirements](#requirements)
- [Installation](#installation)
- [Carthage](#carthage)
- [CocoaPods](#cocoapods)
- [Usage](#usage)
- [Setting up cells and headers](#setting-up-cells-and-headers)
- [Importing MagazineLayout](#importing-magazinelayout)
- [Setting up the collection view](#setting-up-the-collection-view)
- [Registering cells and supplementary views](#registering-cells-and-supplementary-views)
- [Setting the data source](#setting-the-data-source)
- [Configuring the delegate](#configuring-the-delegate)
* [Contributions](#contributions)
* [Maintainers](#maintainers)
* [Contributors](#contributors)
* [License](#license)
## Example App
An example app is available to showcase and enable you to test some of `MagazineLayout`'s features. It can be found in `./Example/MagazineLayoutExample.xcworkspace`.
Note: Make sure to use the `.xcworkspace` file, and not the `.xcodeproj` file, as the latter does not have access to `MagazineLayout.framework`.
### Using the Example App
When you first open the example app, you'll see many items and sections pre-populated. Most items are configured to self-size based on the text they're displaying.
![Example App](Docs/Images/ExampleApp.png)
If you'd like to get rid of the sample content and start with a blank collection view, you can tap the reload icon in the navigation bar.
| Reload Menu | No Items |
| --- | --- |
| ![Reload Menu](Docs/Images/ExampleAppReloadMenu.png) | ![No Items](Docs/Images/ExampleAppBlank.png) |
From this menu, you can also reset the app back to the original sample data.
#### Adding a new item
To add a new item, tap the add icon in the navigation bar.
![Add Item Screen](Docs/Images/ExampleAppAddItem.png)
From the add screen, you can configure a new item to insert into the `UICollectionView`. The item will be inserted with an animation once you tap the done button in the navigation bar.
Item configuration options:
- Section index (will create a new section if one does not exist for the specified index)
- Item index (position in the specified section)
- Item content / text to be displayed in the item (this will change how tall the item is if using a `.dynamic` height mode)
- Color to use for the background of the item
- Width mode (controls how wide the item should be in relation to the available width)
- Height mode (controls self-sizing behavior)
![Add Item Animation](Docs/Images/ExampleAppInsertAnimation.gif)
#### Deleting an item
To delete an item, simple tap on the item in the collection view. The item will be deleted with an animation.
![Delete Item Animation](Docs/Images/ExampleAppDeleteAnimation.gif)
## Getting Started
### Requirements
- Deployment target iOS 10.0+
- Swift 4+
- Xcode 10+
### Installation
#### Carthage
To install `MagazineLayout` using [Carthage](https://github.com/Carthage/Carthage), add
`github "airbnb/MagazineLayout"` to your Cartfile, then follow the integration tutorial [here](https://github.com/Carthage/Carthage#if-youre-building-for-ios-tvos-or-watchos).
#### CocoaPods
To install `MagazineLayout` using [CocoaPods](http://cocoapods.org), add
`pod 'MagazineLayout'` to your Podfile, then follow the integration tutorial [here](https://guides.cocoapods.org/using/using-cocoapods.html).
### Usage
Once you've integrated the `MagazineLayout` into your project, using it with a collection view is easy.
#### Setting up cells and headers
[Due to shortcomings in `UIKit`](MagazineLayout/Public/Views/MagazineLayoutCollectionViewCell.swift#L34-L39), `MagazineLayout` requires its own `UICollectionViewCell` and `UICollectionReusableView` subclasses:
- `MagazineLayoutCollectionViewCell`
- `MagazineLayoutCollectionReusableView`
These two types enable cells and supplementary views to self-size correctly when using `MagazineLayout`. **Make sure that the custom cell and reusable view types in your app subclass from `MagazineLayoutCollectionViewCell` and `MagazineLayoutCollectionReusableView`, respectively.**
Alternatively, you can copy the implementation of `preferredLayoutAttributesFitting(_:)`
for use in your custom cell and reusable view types, without subclassing from the ones `MagazineLayout` provides.
#### Importing MagazineLayout
At the top of the file where you'd like to use `MagazineLayout` (likely a `UIView` or `UIViewController` subclass), import `MagazineLayout`.
```swift
import MagazineLayout
```
#### Setting up the collection view
Create your `UICollectionView` instance, passing in a `MagazineLayout` instance for the layout parameter.
```swift
let layout = MagazineLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
```
Make sure to add `collectionView` as a subview, then properly constrain it using Auto Layout or manually set its `frame` property.
```swift
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
```
#### Registering cells and supplementary views
Register your cell and reusable view types with your collection view.
```swift
collectionView.register(MyCustomCell.self, forCellWithReuseIdentifier: "MyCustomCellReuseIdentifier")
// Only necessary if you want section headers
collectionView.register(MyCustomHeader.self, forSupplementaryViewOfKind: MagazineLayout.SupplementaryViewKind.sectionHeader, withReuseIdentifier: "MyCustomHeaderReuseIdentifier")
// Only necessary if you want section footers
collectionView.register(MyCustomFooter.self, forSupplementaryViewOfKind: MagazineLayout.SupplementaryViewKind.sectionFooter, withReuseIdentifier: "MyCustomFooterReuseIdentifier")
// Only necessary if you want section backgrounds
collectionView.register(MyCustomBackground.self, forSupplementaryViewOfKind: MagazineLayout.SupplementaryViewKind.sectionBackground, withReuseIdentifier: "MyCustomBackgroundReuseIdentifier")
```
Because cells, headers, and footers can self-size (backgrounds do not self-size), in this example, `MyCustomCell`, `MyCustomHeader`, and `MyCustomFooter` **must** have the correct implementation of `preferredLayoutAttributesFitting(_:)`. See [Setting up cells and headers](#setting-up-cells-and-headers).
#### Setting the data source
Now that you've registered your view types with your collection view, it's time to wire up the data source. Like with any collection view integration, your data source needs to conform to `UICollectionViewDataSource`. If the same object that owns your collection view is also your data source, you can simply do this:
```swift
collectionView.dataSource = self
```
#### Configuring the delegate
Lastly, it's time to configure the layout to suit your needs. Like with `UICollectionViewFlowLayout` and `UICollectionViewDelegateFlowLayout`, `MagazineLayout` configured its layout through its `UICollectionViewDelegateMagazineLayout`.
To start configuring `MagazineLayout`, set your collection view's `delegate` property to an object conforming to `UICollectionViewDelegateMagazineLayout`. If the same object that owns your collection view is also your delegate, you can simply do this:
```swift
collectionView.delegate = self
```
Here's an example delegate implementation:
```swift
extension ViewController: UICollectionViewDelegateMagazineLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeModeForItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode {
let widthMode = MagazineLayoutItemWidthMode.halfWidth
let heightMode = MagazineLayoutItemHeightMode.dynamic
return MagazineLayoutItemSizeMode(widthMode: widthMode, heightMode: heightMode)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, visibilityModeForHeaderInSectionAtIndex index: Int) -> MagazineLayoutSupplementaryViewVisibilityMode {
return .visible(heightMode: .dynamic, pinToVisibleBounds: true)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, visibilityModeForFooterInSectionAtIndex index: Int) -> MagazineLayoutSupplementaryViewVisibilityMode {
return .visible(heightMode: .dynamic, pinToVisibleBounds: false)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, visibilityModeForBackgroundInSectionAtIndex index: Int) -> MagazineLayoutBackgroundVisibilityMode {
return .hidden
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, horizontalSpacingForItemsInSectionAtIndex index: Int) -> CGFloat {
return 12
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, verticalSpacingForElementsInSectionAtIndex index: Int) -> CGFloat {
return 12
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForSectionAtIndex index: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 8, bottom: 24, right: 8)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForItemsInSectionAtIndex index: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 24, left: 0, bottom: 24, right: 0)
}
}
```
If you've followed the steps above, you should have a working `UICollectionView` using `MagazineLayout`! If you'd like to work with a pre-made example, check out the included example project, and [the instructions for using it](#example-app).
## Contributions
`MagazineLayout` welcomes both fixes, improvements, and feature additions. If you'd like to contribute, open a pull request with a detailed description of your changes.
As a rule of thumb, if you're proposing an API breaking change or a change to existing functionality, consider proposing it by opening an issue, rather than a pull request; we'll use the issue as a public forum for discussing whether the proposal makes sense or not.
## Maintainers
Bryan Keller
- https://github.com/bryankeller
- https://twitter.com/BKyourway19
Bryn Bodayle
- https://github.com/brynbodayle
- https://twitter.com/brynbodayle
If you or your company has found `MagazineLayout` to be useful, let us know!
## Contributors
`MagazineLayout` would not have been possible without the contributions and support from several of my colleagues at Airbnb. Bryn Bodayle, in particular, has reviewed every PR since `MagazineLayout`'s inception, as well as helped talk through and solve countless tricky `UICollectionView` and `UIKit` issues.
I'd also like to thank the following people, who have all helped pave the way for `MagazineLayout` to be successful:
- Laura Skelton
- Eric Horacek
- Tyler Hedrick
- Michael Bachand
- Xiao Pan
- Yong Li
- Luke Hiesterman
- Jordan Harband
## License
`MagazineLayout` is released under the Apache License 2.0. See [LICENSE](LICENSE) for details.