Archived
Push from command line
This commit is contained in:
Generated
+201
@@ -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
@@ -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?
|
||||
|
||||
}
|
||||
+1085
File diff suppressed because it is too large
Load Diff
+640
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
+134
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+31
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+107
@@ -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]
|
||||
|
||||
}
|
||||
+1053
File diff suppressed because it is too large
Load Diff
Generated
+41
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+27
@@ -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
|
||||
|
||||
}
|
||||
+40
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Generated
Executable
+27
@@ -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"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+27
@@ -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
|
||||
|
||||
}
|
||||
Generated
Executable
+78
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Generated
Executable
+79
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+164
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Generated
Executable
+138
@@ -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
|
||||
|
||||
}
|
||||
Generated
+61
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+88
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+269
@@ -0,0 +1,269 @@
|
||||
# MagazineLayout
|
||||
A collection view layout capable of laying out views in vertically scrolling grids and lists.
|
||||
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://cocoapods.org/pods/MagazineLayout)
|
||||
[](https://cocoapods.org/pods/MagazineLayout)
|
||||
[](https://cocoapods.org/pods/MagazineLayout)
|
||||
[](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 |
|
||||
| --- | --- | --- | --- |
|
||||
|  |  |  |  |
|
||||
|
||||
| Plus Home | Plus Home Tour | Trips | Trip Detail |
|
||||
| --- | --- | --- | --- |
|
||||
| |  |  |  |
|
||||
|
||||
## 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.
|
||||
|
||||

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

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

|
||||
|
||||
#### Deleting an item
|
||||
To delete an item, simple tap on the item in the collection view. The item will be deleted with an animation.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user