: UIViewRepresentable {
30 | var component: C
31 | var proxy: ComponentViewProxy
32 |
33 | func makeUIView(context: Context) -> UIComponentView {
34 | UIComponentView()
35 | }
36 |
37 | func updateUIView(_ uiView: UIComponentView, context: Context) {
38 | uiView.render(component: AnyComponent(component))
39 | proxy.uiView = uiView
40 | }
41 | }
42 |
43 | private final class UIComponentView: UIView, ComponentRenderable {
44 | @available(*, unavailable)
45 | required init?(coder: NSCoder) {
46 | fatalError("init(coder:) has not been implemented")
47 | }
48 |
49 | override init(frame: CGRect) {
50 | super.init(frame: frame)
51 |
52 | backgroundColor = .clear
53 | }
54 |
55 | override var intrinsicContentSize: CGSize {
56 | if let referenceSize = renderedComponent?.referenceSize(in: bounds) {
57 | return referenceSize
58 | }
59 | else if let component = renderedComponent, let content = renderedContent {
60 | return component.intrinsicContentSize(for: content)
61 | }
62 | else {
63 | return super.intrinsicContentSize
64 | }
65 | }
66 | }
67 |
68 | private final class ComponentViewProxy {
69 | var uiView: UIComponentView?
70 | }
71 |
72 | #endif
73 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "idiom" : "ipad",
47 | "size" : "20x20",
48 | "scale" : "1x"
49 | },
50 | {
51 | "idiom" : "ipad",
52 | "size" : "20x20",
53 | "scale" : "2x"
54 | },
55 | {
56 | "idiom" : "ipad",
57 | "size" : "29x29",
58 | "scale" : "1x"
59 | },
60 | {
61 | "idiom" : "ipad",
62 | "size" : "29x29",
63 | "scale" : "2x"
64 | },
65 | {
66 | "idiom" : "ipad",
67 | "size" : "40x40",
68 | "scale" : "1x"
69 | },
70 | {
71 | "idiom" : "ipad",
72 | "size" : "40x40",
73 | "scale" : "2x"
74 | },
75 | {
76 | "idiom" : "ipad",
77 | "size" : "76x76",
78 | "scale" : "1x"
79 | },
80 | {
81 | "idiom" : "ipad",
82 | "size" : "76x76",
83 | "scale" : "2x"
84 | },
85 | {
86 | "idiom" : "ipad",
87 | "size" : "83.5x83.5",
88 | "scale" : "2x"
89 | },
90 | {
91 | "size" : "1024x1024",
92 | "idiom" : "ios-marketing",
93 | "filename" : "1024.png",
94 | "scale" : "1x"
95 | }
96 | ],
97 | "info" : {
98 | "version" : 1,
99 | "author" : "xcode"
100 | }
101 | }
--------------------------------------------------------------------------------
/docs/js/jazzy.search.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | var $typeahead = $('[data-typeahead]');
3 | var $form = $typeahead.parents('form');
4 | var searchURL = $form.attr('action');
5 |
6 | function displayTemplate(result) {
7 | return result.name;
8 | }
9 |
10 | function suggestionTemplate(result) {
11 | var t = '';
12 | t += '' + result.name + '';
13 | if (result.parent_name) {
14 | t += '' + result.parent_name + '';
15 | }
16 | t += '
';
17 | return t;
18 | }
19 |
20 | $typeahead.one('focus', function() {
21 | $form.addClass('loading');
22 |
23 | $.getJSON(searchURL).then(function(searchData) {
24 | const searchIndex = lunr(function() {
25 | this.ref('url');
26 | this.field('name');
27 | this.field('abstract');
28 | for (const [url, doc] of Object.entries(searchData)) {
29 | this.add({url: url, name: doc.name, abstract: doc.abstract});
30 | }
31 | });
32 |
33 | $typeahead.typeahead(
34 | {
35 | highlight: true,
36 | minLength: 3,
37 | autoselect: true
38 | },
39 | {
40 | limit: 10,
41 | display: displayTemplate,
42 | templates: { suggestion: suggestionTemplate },
43 | source: function(query, sync) {
44 | const lcSearch = query.toLowerCase();
45 | const results = searchIndex.query(function(q) {
46 | q.term(lcSearch, { boost: 100 });
47 | q.term(lcSearch, {
48 | boost: 10,
49 | wildcard: lunr.Query.wildcard.TRAILING
50 | });
51 | }).map(function(result) {
52 | var doc = searchData[result.ref];
53 | doc.url = result.ref;
54 | return doc;
55 | });
56 | sync(results);
57 | }
58 | }
59 | );
60 | $form.removeClass('loading');
61 | $typeahead.trigger('focus');
62 | });
63 | });
64 |
65 | var baseURL = searchURL.slice(0, -"search.json".length);
66 |
67 | $typeahead.on('typeahead:select', function(e, result) {
68 | window.location = baseURL + result.url;
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/Tests/Interfaces/ComponentContainerElementTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Carbon
3 |
4 | final class ComponentContainerTests: XCTestCase {
5 | func testContentWillDisplay() {
6 | let container = MockComponentContainer()
7 | let content = UIView()
8 | let component = MockComponent(content: content)
9 |
10 | container.render(component: AnyComponent(component))
11 | container.contentWillDisplay()
12 |
13 | if let captured = component.contentCapturedOnWillDisplay {
14 | XCTAssertEqual(captured, content)
15 | }
16 | else {
17 | XCTFail()
18 | }
19 | }
20 |
21 | func testContentDidEndDisplay() {
22 | let container = MockComponentContainer()
23 | let content = UIView()
24 | let component = MockComponent(content: content)
25 |
26 | container.render(component: AnyComponent(component))
27 | container.contentDidEndDisplay()
28 |
29 | if let captured = component.contentCapturedOnDidEndDisplay {
30 | XCTAssertEqual(captured, content)
31 | }
32 | else {
33 | XCTFail()
34 | }
35 | }
36 |
37 | func testRenderComponent() {
38 | let component = MockComponent(shouldRender: false)
39 | let componentContainerView = UIView()
40 | let container = MockComponentContainer(componentContainerView: componentContainerView)
41 |
42 | container.render(component: AnyComponent(component))
43 |
44 | XCTAssertEqual(component.contentCapturedOnLayout, container.renderedContent as? UIView?)
45 | XCTAssertEqual(component.contentCapturedOnRender, container.renderedContent as? UIView?)
46 | XCTAssertEqual(component, container.renderedComponent?.as(MockComponent.self))
47 | }
48 |
49 | func testShouldRenderComponent() {
50 | let component = A.Component(value: 100)
51 | let container = MockComponentContainer()
52 |
53 | container.render(component: AnyComponent(component))
54 |
55 | let nextComponent = A.Component(value: 200)
56 |
57 | container.render(component: AnyComponent(nextComponent))
58 |
59 | if let renderedComponent = container.renderedComponent?.as(A.Component.self) {
60 | XCTAssertEqual(renderedComponent.value, 100)
61 | }
62 | else {
63 | XCTFail()
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/FunctionBuilder/Group.swift:
--------------------------------------------------------------------------------
1 | /// An affordance for grouping component or section.
2 | ///
3 | /// Example for simple grouping of cells.
4 | ///
5 | /// Group {
6 | /// Label("Cell 0")
7 | ///
8 | /// Label("Cell 1")
9 | ///
10 | /// Label("Cell 2")
11 | /// }
12 | public struct Group {
13 | @usableFromInline
14 | internal var elements: [Element]
15 |
16 | /// Creates a group without elements.
17 | public init() {
18 | elements = []
19 | }
20 | }
21 |
22 | extension Group: CellsBuildable where Element == CellNode {
23 | /// Creates a group with given cells.
24 | ///
25 | /// - Parameter
26 | /// - cells: A closure that constructs cells.
27 | public init(@CellsBuilder cells: () -> C) {
28 | elements = cells().buildCells()
29 | }
30 |
31 | /// Creates a group with cells mapped from given elements.
32 | ///
33 | /// - Parameter
34 | /// - data: The sequence of elements to be mapped to cells.
35 | /// - cell: A closure to create a cell with passed element.
36 | public init(of data: Data, cell: (Data.Element) -> C) {
37 | elements = data.flatMap { element in
38 | cell(element).buildCells()
39 | }
40 | }
41 |
42 | /// Build an array of cell.
43 | ///
44 | /// - Returns: An array of cell.
45 | public func buildCells() -> [CellNode] {
46 | elements
47 | }
48 | }
49 |
50 | extension Group: SectionsBuildable where Element == Section {
51 | /// Creates a group with given sections.
52 | ///
53 | /// - Parameter
54 | /// - sections: A closure that constructs sections.
55 | public init(@SectionsBuilder sections: () -> S) {
56 | elements = sections().buildSections()
57 | }
58 |
59 | /// Creates a group with sections mapped from given elements.
60 | ///
61 | /// - Parameter
62 | /// - data: The sequence of elements to be mapped to sections.
63 | /// - section: A closure to create a section with passed element.
64 | public init(of data: Data, section: (Data.Element) -> S) {
65 | elements = data.flatMap { element in
66 | section(element).buildSections()
67 | }
68 | }
69 |
70 | /// Build an array of section.
71 | ///
72 | /// - Returns: An array of section.
73 | public func buildSections() -> [Section] {
74 | elements
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Top/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import Carbon
4 |
5 | final class HomeViewController: UIViewController {
6 | enum Destination {
7 | case hello
8 | case pangram
9 | case kyoto
10 | case emoji
11 | case todo
12 | case form
13 | case kyotoSwiftUI
14 | }
15 |
16 | @IBOutlet var tableView: UITableView!
17 |
18 | private let renderer = Renderer(
19 | adapter: UITableViewAdapter(),
20 | updater: UITableViewUpdater()
21 | )
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | title = "Home"
27 | renderer.target = tableView
28 |
29 | renderer.render {
30 | Header("EXAMPLES")
31 | .identified(by: \.title)
32 |
33 | HomeItem(title: "👋 Hello") { [weak self] in
34 | self?.push(.hello)
35 | }
36 |
37 | HomeItem(title: "🔠 Pangram") { [weak self] in
38 | self?.push(.pangram)
39 | }
40 |
41 | HomeItem(title: "⛩ Kyoto") { [weak self] in
42 | self?.push(.kyoto)
43 | }
44 |
45 | HomeItem(title: "😀 Shuffle Emoji") { [weak self] in
46 | self?.push(.emoji)
47 | }
48 |
49 | HomeItem(title: "📋 Todo App") { [weak self] in
50 | self?.push(.todo)
51 | }
52 |
53 | HomeItem(title: "👤 Profile Form") { [weak self] in
54 | self?.push(.form)
55 | }
56 |
57 | HomeItem(title: "⛩ Kyoto SwiftUI") { [weak self] in
58 | self?.push(.kyotoSwiftUI)
59 | }
60 | }
61 | }
62 |
63 | func push(_ destination: Destination) {
64 | let controller: UIViewController
65 |
66 | switch destination {
67 | case .hello:
68 | controller = HelloViewController()
69 |
70 | case .pangram:
71 | controller = PangramViewController()
72 |
73 | case .kyoto:
74 | controller = KyotoViewController()
75 |
76 | case .emoji:
77 | controller = EmojiViewController()
78 |
79 | case .todo:
80 | controller = TodoViewController()
81 |
82 | case .form:
83 | controller = FormViewController()
84 |
85 | case .kyotoSwiftUI:
86 | controller = HostingController(rootView: KyotoSwiftUIView())
87 | }
88 |
89 | navigationController?.pushViewController(controller, animated: true)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/Nodes/CellNode.swift:
--------------------------------------------------------------------------------
1 | import DifferenceKit
2 |
3 | /// The node for cell that can be uniquely identified.
4 | /// Wrapping type-erased identifier and component.
5 | /// This works as an intermediary for `DifferenceKit`.
6 | public struct CellNode {
7 | /// A type-erased identifier that can be used to uniquely
8 | /// identify the component.
9 | public var id: AnyHashable
10 |
11 | /// A type-erased component which wrapped in `self`.
12 | public var component: AnyComponent
13 |
14 | /// Create a node wrapping given id and component.
15 | ///
16 | /// - Parameters:
17 | /// - id: An identifier to be wrapped.
18 | /// - component: A component to be wrapped.
19 | public init(id: I, _ component: C) {
20 | // This is workaround for avoid redundant `AnyHashable` wrapping.
21 | if type(of: id) == AnyHashable.self {
22 | self.id = unsafeBitCast(id, to: AnyHashable.self)
23 | }
24 | else {
25 | self.id = id
26 | }
27 |
28 | self.component = AnyComponent(component)
29 | }
30 |
31 | /// Create a node wrapping given component and its id.
32 | ///
33 | /// - Parameter
34 | /// - component: A component to be wrapped that can be uniquely identified.
35 | @inlinable
36 | public init(_ component: C) {
37 | self.init(id: component.id, component)
38 | }
39 |
40 | /// Returns a base instance of component casted as given type if possible.
41 | ///
42 | /// - Parameter: An expected type of the base instance of component to casted.
43 | /// - Returns: A casted base instance.
44 | @inlinable
45 | public func component(as _: T.Type) -> T? {
46 | return component.as(T.self)
47 | }
48 | }
49 |
50 | extension CellNode: CellsBuildable {
51 | public func buildCells() -> [CellNode] {
52 | return [self]
53 | }
54 | }
55 |
56 | extension CellNode: Differentiable {
57 | /// An identifier value for difference calculation.
58 | @inlinable
59 | public var differenceIdentifier: AnyHashable {
60 | return id
61 | }
62 |
63 | /// Indicate whether the content of `self` is equals to the content of
64 | /// the given source value.
65 | @inlinable
66 | public func isContentEqual(to source: CellNode) -> Bool {
67 | return !source.component.shouldContentUpdate(with: component)
68 | }
69 | }
70 |
71 | extension CellNode: CustomDebugStringConvertible {
72 | /// A textual representation of this instance, suitable for debugging.
73 | @inlinable
74 | public var debugDescription: String {
75 | return "CellNode(id: \(id), component: \(component))"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Tests/Nodes/CellNodeTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Carbon
3 |
4 | final class CellNodeTests: XCTestCase {
5 | func testInitWithComponent() {
6 | let component = A.Component()
7 | let node = CellNode(id: TestID.a, component)
8 |
9 | XCTAssertEqual(node.id.base as? TestID, .a)
10 | XCTAssertEqual(node.component.as(A.Component.self), component)
11 | }
12 |
13 | func testInitWithIdentifiedComponentWrapper() {
14 | let component = A.Component()
15 | let node = CellNode(component.identified(by: \.value))
16 |
17 | XCTAssertEqual(node.id.base as? Int, component.value)
18 | XCTAssertEqual(node.component.as(IdentifiedComponentWrapper.self)?.wrapped, component)
19 | }
20 |
21 | func testInitWithIdentifiableComponent() {
22 | let component = MockIdentifiableComponent(id: TestID.a)
23 | let node = CellNode(component)
24 |
25 | XCTAssertEqual(node.id.base as? TestID, .a)
26 | XCTAssertEqual(node.component.as(MockIdentifiableComponent.self), component)
27 | }
28 |
29 | func testComponentCasting() {
30 | let component = A.Component()
31 | let node = CellNode(id: TestID.a, component)
32 |
33 | XCTAssertNil(node.component(as: Never.self))
34 | XCTAssertNotNil(node.component(as: A.Component.self))
35 | }
36 |
37 | func testContentEquatableConformance() {
38 | let component1 = MockComponent(shouldContentUpdate: true)
39 | let component2 = MockComponent(shouldContentUpdate: false)
40 | let node1 = CellNode(id: TestID.a, component1)
41 | let node2 = CellNode(id: TestID.b, component2)
42 |
43 | XCTAssertEqual(node1.isContentEqual(to: node1), false)
44 | XCTAssertEqual(node2.isContentEqual(to: node2), true)
45 | }
46 |
47 | func testDifferentiableConformance() {
48 | let component1 = MockComponent()
49 | let component2 = MockIdentifiableComponent(id: TestID.b)
50 | let node1 = CellNode(id: TestID.a, component1)
51 | let node2 = CellNode(component2)
52 | let node3 = CellNode(id: TestID.c, component2)
53 |
54 | XCTAssertEqual(node1.differenceIdentifier.base as? TestID, .a)
55 | XCTAssertEqual(node2.differenceIdentifier.base as? TestID, .b)
56 | XCTAssertEqual(node3.differenceIdentifier.base as? TestID, .c)
57 | }
58 |
59 | func testAvoidRedundantAnyHashableWrappingForID() {
60 | let component = MockIdentifiableComponent(id: AnyHashable(TestID.a))
61 | let node1 = CellNode(component)
62 | let node2 = CellNode(id: AnyHashable(TestID.b), component)
63 |
64 | XCTAssertEqual(node1.id, component.id)
65 | XCTAssertEqual(node2.id, TestID.b)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Common/FooterContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Tests/FunctionBuilder/GroupTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Carbon
3 |
4 | final class GroupTests: XCTestCase {
5 | func testInitWithoutElements() {
6 | let group = Group()
7 | XCTAssertTrue(group.elements.isEmpty)
8 | }
9 |
10 | func testCellsWithFunctionBuilder() {
11 | let componentA = A.Component()
12 | let componentB = B.Component()
13 | let componentA2 = A.Component()
14 | let componentB2 = B.Component()
15 | let appearsComponentB2 = false
16 |
17 | let group = Group {
18 | componentA
19 | componentB
20 |
21 | if true {
22 | componentA2
23 | }
24 |
25 | if appearsComponentB2 {
26 | componentB2
27 | }
28 | }
29 |
30 | let cells = group.buildCells()
31 |
32 | XCTAssertEqual(cells.count, 3)
33 | XCTAssertEqual(cells[0].component(as: A.Component.self), componentA)
34 | XCTAssertEqual(cells[1].component(as: B.Component.self), componentB)
35 | XCTAssertEqual(cells[2].component(as: A.Component.self), componentA2)
36 | }
37 |
38 | func testCellsWithSequence() {
39 | let range = 0..<3
40 | let group = Group(of: range) { value in
41 | A.Component(value: value)
42 | }
43 |
44 | let cells = group.buildCells()
45 |
46 | XCTAssertEqual(cells.count, 3)
47 | XCTAssertEqual(cells[0].component(as: A.Component.self)?.value, 0)
48 | XCTAssertEqual(cells[1].component(as: A.Component.self)?.value, 1)
49 | XCTAssertEqual(cells[2].component(as: A.Component.self)?.value, 2)
50 | }
51 |
52 | func testSectionsWithFunctionBuilder() {
53 | let section0 = Section(id: 0)
54 | let section1 = Section(id: 1)
55 | let section2 = Section(id: 2)
56 | let section3 = Section(id: 3)
57 | let appearsSection3 = false
58 |
59 | let group = Group {
60 | section0
61 | section1
62 |
63 | if true {
64 | section2
65 | }
66 |
67 | if appearsSection3 {
68 | section3
69 | }
70 | }
71 |
72 | let sections = group.buildSections()
73 |
74 | XCTAssertEqual(sections.count, 3)
75 | XCTAssertEqual(sections[0].id, 0)
76 | XCTAssertEqual(sections[1].id, 1)
77 | XCTAssertEqual(sections[2].id, 2)
78 | }
79 |
80 | func testSectionsWithSequence() {
81 | let range = 0..<3
82 | let group = Group(of: range) { value in
83 | Section(id: value)
84 | }
85 |
86 | let sections = group.buildSections()
87 |
88 | XCTAssertEqual(sections.count, 3)
89 | XCTAssertEqual(sections[0].id, 0)
90 | XCTAssertEqual(sections[1].id, 1)
91 | XCTAssertEqual(sections[2].id, 2)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Todo/TodoEmptyContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Form/FormTextPickerContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.0)
5 | activesupport (4.2.11.1)
6 | i18n (~> 0.7)
7 | minitest (~> 5.1)
8 | thread_safe (~> 0.3, >= 0.3.4)
9 | tzinfo (~> 1.1)
10 | atomos (0.1.3)
11 | claide (1.0.2)
12 | cocoapods (1.7.5)
13 | activesupport (>= 4.0.2, < 5)
14 | claide (>= 1.0.2, < 2.0)
15 | cocoapods-core (= 1.7.5)
16 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
17 | cocoapods-downloader (>= 1.2.2, < 2.0)
18 | cocoapods-plugins (>= 1.0.0, < 2.0)
19 | cocoapods-search (>= 1.0.0, < 2.0)
20 | cocoapods-stats (>= 1.0.0, < 2.0)
21 | cocoapods-trunk (>= 1.3.1, < 2.0)
22 | cocoapods-try (>= 1.1.0, < 2.0)
23 | colored2 (~> 3.1)
24 | escape (~> 0.0.4)
25 | fourflusher (>= 2.3.0, < 3.0)
26 | gh_inspector (~> 1.0)
27 | molinillo (~> 0.6.6)
28 | nap (~> 1.0)
29 | ruby-macho (~> 1.4)
30 | xcodeproj (>= 1.10.0, < 2.0)
31 | cocoapods-core (1.7.5)
32 | activesupport (>= 4.0.2, < 6)
33 | fuzzy_match (~> 2.0.4)
34 | nap (~> 1.0)
35 | cocoapods-deintegrate (1.0.4)
36 | cocoapods-downloader (1.2.2)
37 | cocoapods-plugins (1.0.0)
38 | nap
39 | cocoapods-search (1.0.0)
40 | cocoapods-stats (1.1.0)
41 | cocoapods-trunk (1.3.1)
42 | nap (>= 0.8, < 2.0)
43 | netrc (~> 0.11)
44 | cocoapods-try (1.1.0)
45 | colored2 (3.1.2)
46 | concurrent-ruby (1.1.5)
47 | escape (0.0.4)
48 | ffi (1.11.1)
49 | fourflusher (2.3.1)
50 | fuzzy_match (2.0.4)
51 | gh_inspector (1.1.3)
52 | i18n (0.9.5)
53 | concurrent-ruby (~> 1.0)
54 | jazzy (0.10.0)
55 | cocoapods (~> 1.5)
56 | mustache (~> 1.1)
57 | open4
58 | redcarpet (~> 3.4)
59 | rouge (>= 2.0.6, < 4.0)
60 | sass (~> 3.6)
61 | sqlite3 (~> 1.3)
62 | xcinvoke (~> 0.3.0)
63 | liferaft (0.0.6)
64 | minitest (5.11.3)
65 | molinillo (0.6.6)
66 | mustache (1.1.0)
67 | nanaimo (0.2.6)
68 | nap (1.1.0)
69 | netrc (0.11.0)
70 | open4 (1.3.4)
71 | rb-fsevent (0.10.3)
72 | rb-inotify (0.10.0)
73 | ffi (~> 1.0)
74 | redcarpet (3.5.0)
75 | rouge (3.10.0)
76 | ruby-macho (1.4.0)
77 | sass (3.7.4)
78 | sass-listen (~> 4.0.0)
79 | sass-listen (4.0.0)
80 | rb-fsevent (~> 0.9, >= 0.9.4)
81 | rb-inotify (~> 0.9, >= 0.9.7)
82 | sqlite3 (1.4.1)
83 | thread_safe (0.3.6)
84 | tzinfo (1.2.5)
85 | thread_safe (~> 0.1)
86 | xcinvoke (0.3.0)
87 | liferaft (~> 0.0.6)
88 | xcodeproj (1.11.0)
89 | CFPropertyList (>= 2.3.3, < 4.0)
90 | atomos (~> 0.1.3)
91 | claide (>= 1.0.2, < 2.0)
92 | colored2 (~> 3.1)
93 | nanaimo (~> 0.2.6)
94 |
95 | PLATFORMS
96 | ruby
97 |
98 | DEPENDENCIES
99 | cocoapods (= 1.7.5)
100 | jazzy (= 0.10.0)
101 |
102 | BUNDLED WITH
103 | 1.17.1
104 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Form/FormDatePickerContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Common/HeaderContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Form/FormViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Hello/HelloViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Kyoto/KyotoLicenseContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Top/HomeViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Hello/HelloMessageContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Emoji/EmojiLabelContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Tests/Internal/UIScrollViewExtensionsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Carbon
3 |
4 | final class MockScrollViewExtensionsTests: XCTestCase {
5 | func testIsScrolling() {
6 | let scrollView1 = MockScrollView()
7 | scrollView1._isTracking = true
8 |
9 | let scrollView2 = MockScrollView()
10 | scrollView2._isDragging = true
11 |
12 | let scrollView3 = MockScrollView()
13 | scrollView3._isDecelerating = true
14 |
15 | XCTAssertTrue(scrollView1._isScrolling)
16 | XCTAssertTrue(scrollView2._isScrolling)
17 | XCTAssertTrue(scrollView3._isScrolling)
18 | }
19 |
20 | func testSetAdjustedContentOffsetIfNeeded() {
21 | let scrollView1 = MockScrollView()
22 | let expectedContentOffset1 = CGPoint(x: 50, y: 50)
23 | scrollView1.contentInset = UIEdgeInsets(top: 1, left: 2, bottom: 3, right: 4)
24 | scrollView1.contentSize = CGSize(width: 100, height: 200)
25 | scrollView1.bounds.size = CGSize(width: 30, height: 40)
26 | scrollView1.contentOffset = CGPoint(x: 10, y: 20)
27 | scrollView1._setAdjustedContentOffsetIfNeeded(expectedContentOffset1)
28 | XCTAssertEqual(scrollView1.contentOffset, expectedContentOffset1)
29 |
30 | let scrollView2 = MockScrollView()
31 | scrollView2.contentInset = UIEdgeInsets(top: 1, left: 2, bottom: 3, right: 4)
32 | scrollView2.contentSize = CGSize(width: 100, height: 20)
33 | scrollView2.bounds.size = CGSize(width: 30, height: 40)
34 | scrollView2.contentOffset = CGPoint(x: 10, y: 20)
35 | scrollView2._setAdjustedContentOffsetIfNeeded(CGPoint(x: 50, y: 50))
36 | XCTAssertEqual(scrollView2.contentOffset, CGPoint(x: 10, y: 20))
37 |
38 | let scrollView3 = MockScrollView()
39 | scrollView3._isTracking = true
40 | scrollView3._setAdjustedContentOffsetIfNeeded(CGPoint(x: 50, y: 50))
41 | XCTAssertEqual(scrollView3.contentOffset, .zero)
42 |
43 | let scrollView4 = MockScrollView()
44 | scrollView4._isDragging = true
45 | scrollView4._setAdjustedContentOffsetIfNeeded(CGPoint(x: 50, y: 50))
46 | XCTAssertEqual(scrollView4.contentOffset, .zero)
47 |
48 | let scrollView5 = MockScrollView()
49 | scrollView5._isDecelerating = true
50 | scrollView5._setAdjustedContentOffsetIfNeeded(CGPoint(x: 50, y: 50))
51 | XCTAssertEqual(scrollView5.contentOffset, .zero)
52 |
53 | let scrollView6 = MockScrollView()
54 | scrollView6.contentInset = UIEdgeInsets(top: 1, left: 2, bottom: 3, right: 4)
55 | scrollView6.contentSize = CGSize(width: 200, height: 200)
56 | scrollView6.bounds.size = CGSize(width: 30, height: 40)
57 | scrollView6.contentOffset = CGPoint(x: 10, y: 20)
58 | scrollView6._setAdjustedContentOffsetIfNeeded(CGPoint(x: 100, y: 100))
59 | XCTAssertEqual(scrollView6.contentOffset, CGPoint(x: 100, y: 100))
60 |
61 | let scrollView7 = MockScrollView()
62 | scrollView7.contentInset = UIEdgeInsets(top: 1, left: 2, bottom: 3, right: 4)
63 | scrollView7.contentSize = CGSize(width: 100, height: 200)
64 | scrollView7.bounds.size = CGSize(width: 30, height: 40)
65 | scrollView7.contentOffset = CGPoint(x: 10, y: 20)
66 | scrollView7._setAdjustedContentOffsetIfNeeded(CGPoint(x: 100, y: 200))
67 | XCTAssertEqual(scrollView7.contentOffset, CGPoint(x: 74, y: 163))
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/Interfaces/ComponentRenderable.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /// Represents a container that can render a component.
4 | public protocol ComponentRenderable: class {
5 | /// The container view to be render a component.
6 | var componentContainerView: UIView { get }
7 | }
8 |
9 | private let renderedContentAssociation = RuntimeAssociation(default: nil)
10 | private let renderedComponentAssociation = RuntimeAssociation(default: nil)
11 |
12 | public extension ComponentRenderable {
13 | /// A content of component that rendered on container.
14 | private(set) var renderedContent: Any? {
15 | get { return renderedContentAssociation[self] }
16 | set { renderedContentAssociation[self] = newValue }
17 | }
18 |
19 | /// A component that latest rendered on container.
20 | private(set) var renderedComponent: AnyComponent? {
21 | get { return renderedComponentAssociation[self] }
22 | set { renderedComponentAssociation[self] = newValue }
23 | }
24 | }
25 |
26 | internal extension ComponentRenderable {
27 | /// Invoked every time of before a component got into visible area.
28 | func contentWillDisplay() {
29 | guard let content = renderedContent else { return }
30 |
31 | renderedComponent?.contentWillDisplay(content)
32 | }
33 |
34 | /// Invoked every time of after a component went out from visible area.
35 | func contentDidEndDisplay() {
36 | guard let content = renderedContent else { return }
37 |
38 | renderedComponent?.contentDidEndDisplay(content)
39 | }
40 |
41 | /// Render given componet to container.
42 | ///
43 | /// - Parameter:
44 | /// - component: A component to be rendered.
45 | func render(component: AnyComponent) {
46 | switch (renderedContent, renderedComponent) {
47 | case (let content?, let renderedComponent?) where !renderedComponent.shouldRender(next: component, in: content):
48 | break
49 |
50 | case (let content?, _):
51 | component.render(in: content)
52 | renderedComponent = component
53 |
54 | case (nil, _):
55 | let content = component.renderContent()
56 | component.layout(content: content, in: componentContainerView)
57 | renderedContent = content
58 | render(component: component)
59 | }
60 | }
61 | }
62 |
63 | public extension ComponentRenderable where Self: UIView {
64 | /// The container view to be render a component.
65 | var componentContainerView: UIView {
66 | return self
67 | }
68 | }
69 |
70 | public extension ComponentRenderable where Self: UITableViewCell {
71 | /// The container view to be render a component.
72 | var componentContainerView: UIView {
73 | return contentView
74 | }
75 | }
76 |
77 | public extension ComponentRenderable where Self: UITableViewHeaderFooterView {
78 | /// The container view to be render a component.
79 | var componentContainerView: UIView {
80 | return contentView
81 | }
82 | }
83 |
84 | public extension ComponentRenderable where Self: UICollectionViewCell {
85 | /// The container view to be render a component.
86 | var componentContainerView: UIView {
87 | return contentView
88 | }
89 | }
90 |
91 | public extension ComponentRenderable where Self: UICollectionReusableView {
92 | /// The container view to be render a component.
93 | var componentContainerView: UIView {
94 | return self
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Kyoto/KyotoViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
--------------------------------------------------------------------------------
/Tests/SwiftUISupport/ComponentSwiftUISupportTests.swift:
--------------------------------------------------------------------------------
1 | #if canImport(SwiftUI) && canImport(Combine)
2 |
3 | import XCTest
4 | import SwiftUI
5 | @testable import Carbon
6 |
7 | final class ComponentSwiftUISupportTests: XCTestCase {
8 | func testDisplayLifecycle() {
9 | guard #available(iOS 13.0, *) else {
10 | return
11 | }
12 |
13 | struct TestComponent: Component, View {
14 | var willDisplay: () -> Void
15 | var didEndDisplay: () -> Void
16 |
17 | func renderContent() -> UIView {
18 | UIView()
19 | }
20 |
21 | func render(in content: UIView) {}
22 |
23 | func contentWillDisplay(_ content: UIView) {
24 | willDisplay()
25 | }
26 |
27 | func contentDidEndDisplay(_ content: UIView) {
28 | didEndDisplay()
29 | }
30 | }
31 |
32 | var isWillDisplayCalled = false
33 | var isDidEndDisplayCalled = false
34 | let component = TestComponent(
35 | willDisplay: { isWillDisplayCalled = true },
36 | didEndDisplay: { isDidEndDisplayCalled = true }
37 | )
38 | let hostingController = UIHostingController(rootView: AnyView(component.body))
39 |
40 | XCTAssertFalse(isWillDisplayCalled)
41 | XCTAssertFalse(isDidEndDisplayCalled)
42 |
43 | let window = UIWindow()
44 | window.rootViewController = hostingController
45 | window.isHidden = false
46 | window.setNeedsLayout()
47 | window.layoutIfNeeded()
48 |
49 | XCTAssertTrue(isWillDisplayCalled)
50 | XCTAssertFalse(isDidEndDisplayCalled)
51 |
52 | hostingController.rootView = AnyView(EmptyView())
53 | window.setNeedsLayout()
54 | window.layoutIfNeeded()
55 |
56 | XCTAssertTrue(isWillDisplayCalled)
57 | XCTAssertTrue(isDidEndDisplayCalled)
58 | }
59 |
60 | func testReferenceSize() {
61 | guard #available(iOS 13.0, *) else {
62 | return
63 | }
64 |
65 | struct TestComponent: Component, View {
66 | static let testSize = CGSize(width: 123, height: 456)
67 |
68 | func renderContent() -> UIView {
69 | UIView()
70 | }
71 |
72 | func render(in content: UIView) {}
73 |
74 | func referenceSize(in bounds: CGRect) -> CGSize? {
75 | Self.testSize
76 | }
77 | }
78 |
79 | let component = TestComponent()
80 | let hostingController = UIHostingController(rootView: component.body)
81 |
82 | XCTAssertEqual(hostingController.view.sizeThatFits(.zero), TestComponent.testSize)
83 | }
84 |
85 | func testIntrinsicContentSize() {
86 | guard #available(iOS 13.0, *) else {
87 | return
88 | }
89 |
90 | struct TestComponent: Component, View {
91 | static let testSize = CGSize(width: 123, height: 456)
92 |
93 | func renderContent() -> UIView {
94 | UIView()
95 | }
96 |
97 | func render(in content: UIView) {}
98 |
99 | func intrinsicContentSize(for content: UIView) -> CGSize {
100 | Self.testSize
101 | }
102 | }
103 |
104 | let component = TestComponent()
105 | let hostingController = UIHostingController(rootView: component.body)
106 |
107 | XCTAssertEqual(hostingController.view.sizeThatFits(.zero), TestComponent.testSize)
108 | }
109 | }
110 |
111 | #endif
112 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Example-iOS.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Carbon.xcodeproj/xcshareddata/xcschemes/Carbon.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
42 |
48 |
49 |
50 |
51 |
52 |
62 |
63 |
69 |
70 |
71 |
72 |
78 |
79 |
85 |
86 |
87 |
88 |
90 |
91 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Form/FormTextViewContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Kyoto/KyotoMagazineLayoutAdapter.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Carbon
3 | import MagazineLayout
4 |
5 | extension MagazineLayoutCollectionViewCell: ComponentRenderable {}
6 |
7 | final class KyotoMagazineLayoutAdapter: UICollectionViewAdapter, UICollectionViewDelegateMagazineLayout {
8 | override func cellRegistration(collectionView: UICollectionView, indexPath: IndexPath, node: CellNode) -> CellRegistration {
9 | CellRegistration(class: MagazineLayoutCollectionViewCell.self)
10 | }
11 |
12 | override func supplementaryViewNode(forElementKind kind: String, collectionView: UICollectionView, at indexPath: IndexPath) -> ViewNode? {
13 | switch kind {
14 | case MagazineLayout.SupplementaryViewKind.sectionHeader:
15 | return headerNode(in: indexPath.section)
16 |
17 | case MagazineLayout.SupplementaryViewKind.sectionFooter:
18 | return footerNode(in: indexPath.section)
19 |
20 | default:
21 | return super.supplementaryViewNode(forElementKind: kind, collectionView: collectionView, at: indexPath)
22 | }
23 | }
24 |
25 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeModeForItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode {
26 | let node = cellNode(at: indexPath)
27 | guard let size = node.component.referenceSize(in: collectionView.bounds) else {
28 | return MagazineLayoutItemSizeMode(widthMode: .halfWidth, heightMode: .dynamic)
29 | }
30 |
31 | return MagazineLayoutItemSizeMode(widthMode: .halfWidth, heightMode: .static(height: size.height))
32 | }
33 |
34 | func collectionView(
35 | _ collectionView: UICollectionView,
36 | layout collectionViewLayout: UICollectionViewLayout,
37 | visibilityModeForHeaderInSectionAtIndex index: Int
38 | ) -> MagazineLayoutHeaderVisibilityMode {
39 | guard let node = headerNode(in: index) else {
40 | return .hidden
41 | }
42 |
43 | guard let referenceSize = node.component.referenceSize(in: collectionView.bounds) else {
44 | return .visible(heightMode: .dynamic)
45 | }
46 |
47 | return .visible(heightMode: .static(height: referenceSize.height))
48 | }
49 |
50 | func collectionView(
51 | _ collectionView: UICollectionView,
52 | layout collectionViewLayout: UICollectionViewLayout,
53 | visibilityModeForFooterInSectionAtIndex index: Int
54 | ) -> MagazineLayoutFooterVisibilityMode {
55 | guard let node = footerNode(in: index) else {
56 | return .hidden
57 | }
58 |
59 | guard let referenceSize = node.component.referenceSize(in: collectionView.bounds) else {
60 | return .visible(heightMode: .dynamic)
61 | }
62 |
63 | return .visible(heightMode: .static(height: referenceSize.height))
64 | }
65 |
66 | func collectionView(
67 | _ collectionView: UICollectionView,
68 | layout collectionViewLayout: UICollectionViewLayout,
69 | visibilityModeForBackgroundInSectionAtIndex index: Int
70 | ) -> MagazineLayoutBackgroundVisibilityMode {
71 | .hidden
72 | }
73 |
74 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, horizontalSpacingForItemsInSectionAtIndex index: Int) -> CGFloat {
75 | 16
76 | }
77 |
78 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, verticalSpacingForElementsInSectionAtIndex index: Int) -> CGFloat {
79 | 16
80 | }
81 |
82 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForSectionAtIndex index: Int) -> UIEdgeInsets {
83 | .zero
84 | }
85 |
86 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForItemsInSectionAtIndex index: Int) -> UIEdgeInsets {
87 | UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/Renderer.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /// Renderer is a controller to render passed data to target
4 | /// immediately using specific adapter and updater.
5 | ///
6 | /// Its behavior can be changed by using other type of adapter,
7 | /// updater, or by customizing it.
8 | ///
9 | /// Example for render a section containing simple nodes.
10 | ///
11 | /// let tableView: UITableView = ...
12 | /// let renderer = Renderer(
13 | /// adapter: UITableViewAdapter(),
14 | /// updater: UITableViewUpdater()
15 | /// )
16 | ///
17 | /// renderer.target = tableView
18 | ///
19 | /// renderer.render {
20 | /// Label("Cell 1")
21 | /// .identified(by: \.text)
22 | ///
23 | /// Label("Cell 2")
24 | /// .identified(by: \.text)
25 | ///
26 | /// Label("Cell 3")
27 | /// .identified(by: \.text)
28 | /// }
29 | open class Renderer {
30 | /// An instance of adapter that specified at initialized.
31 | public let adapter: Updater.Adapter
32 |
33 | /// An instance of updater that specified at initialized.
34 | public let updater: Updater
35 |
36 | /// An instance of target that weakly referenced.
37 | /// It will be passed to the `prepare` method of updater at didSet.
38 | open weak var target: Updater.Target? {
39 | didSet {
40 | guard let target = target else { return }
41 | updater.prepare(target: target, adapter: adapter)
42 | }
43 | }
44 |
45 | /// Returns a current data held in adapter.
46 | /// When data is set, it renders to the target immediately.
47 | open var data: [Section] {
48 | get { return adapter.data }
49 | set(data) { render(data) }
50 | }
51 |
52 | /// Create a new instance with given adapter and updater.
53 | public init(adapter: Updater.Adapter, updater: Updater) {
54 | self.adapter = adapter
55 | self.updater = updater
56 | }
57 |
58 | /// Render given collection of sections, immediately.
59 | ///
60 | /// - Parameters:
61 | /// - data: A collection of sections to be rendered.
62 | open func render(_ data: C) where C.Element == Section {
63 | let data = Array(data)
64 |
65 | guard let target = target else {
66 | adapter.data = data
67 | return
68 | }
69 |
70 | updater.performUpdates(target: target, adapter: adapter, data: data)
71 | }
72 |
73 | /// Render given collection of sections skipping nil, immediately.
74 | ///
75 | /// - Parameters:
76 | /// - data: A collection of sections to be rendered that can be contains nil.
77 | open func render(_ data: C) where C.Element == Section? {
78 | render(data.compactMap { $0 })
79 | }
80 |
81 | /// Render given collection sections, immediately.
82 | ///
83 | /// - Parameters:
84 | /// - data: A variadic number of sections to be rendered.
85 | open func render(_ data: Section...) {
86 | render(data)
87 | }
88 |
89 | /// Render given variadic number of sections skipping nil, immediately.
90 | ///
91 | /// - Parameters:
92 | /// - data: A variadic number of sections to be rendered that can be contains nil.
93 | open func render(_ data: Section?...) {
94 | render(data.compactMap { $0 })
95 | }
96 |
97 | /// Render given variadic number of sections with function builder syntax, immediately.
98 | ///
99 | /// - Parameters:
100 | /// - sections: A closure that constructs sections.
101 | open func render(@SectionsBuilder sections: () -> S) {
102 | render(sections().buildSections())
103 | }
104 |
105 | /// Render a single section contains given cells with function builder syntax, immediately.
106 | ///
107 | /// - Parameters:
108 | /// - cells: A closure that constructs cells.
109 | open func render(@CellsBuilder cells: () -> C) {
110 | render {
111 | Section(id: UniqueIdentifier(), cells: cells)
112 | }
113 | }
114 | }
115 |
116 | private struct UniqueIdentifier: Hashable {}
117 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Kyoto/KyotoImageContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 🚴 Contributing to Carbon
2 |
3 | First of all, thanks for your interest in Carbon.
4 |
5 | There are several ways to contribute to this project. We welcome contributions in all ways.
6 | We have made some contribution guidelines to smoothly incorporate your opinions and code into this project.
7 |
8 | ## 📝 Open Issue
9 |
10 | When you found a bug or having a feature request in Carbon, search for the issue from the [existing](https://github.com/ra1028/Carbon/issues) and feel free to open the issue after making sure it isn't already reported.
11 |
12 | In order to we understand your issue accurately, please include as much information as possible in the issue template.
13 | The screenshot are also big clue to understand the issue.
14 |
15 | If you know exactly how to fix the bug you report or implement the feature you propose, please pull request instead of an issue.
16 |
17 | ## 🚀 Pull Request
18 |
19 | We are waiting for a pull request to make this project more better with us.
20 | If you want to add a new feature, let's discuss about it first on issue.
21 |
22 | ### Getting Started
23 |
24 | To run project, install dependencies and open workspace as following commands.
25 |
26 | ```bash
27 | $ git clone https://github.com/ra1028/Carbon.git
28 | $ cd Carbon/
29 | $ make setup
30 | $ open Carbon.xcworkspace
31 | ```
32 |
33 | ### Lint
34 |
35 | Please introduce [SwiftLint](https://github.com/realm/SwiftLint) into your environment before start writing the code.
36 | Xcode automatically runs lint in the build phase.
37 |
38 | The code written according to lint should match our coding style, but for particular cases where style is unknown, refer to the existing code base.
39 |
40 | ### Test
41 |
42 | The test will tells us the validity of your code.
43 | All codes entering the master must pass the all tests.
44 | If you change the code or add new features, you should add tests.
45 |
46 | ### Documentation
47 |
48 | Please write the document using [Xcode markup](https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_markup_formatting_ref/) to the code you added.
49 | Documentation template is inserted automatically by using Xcode shortcut **⌥⌘/**.
50 | Our document style is slightly different from the template. The example is below.
51 | ```swift
52 | /// The example class for documentation.
53 | final class Foo {
54 | /// A property value.
55 | let prop: Int
56 |
57 | /// Create a new foo with a param.
58 | ///
59 | /// - Parameters:
60 | /// - param: An Int value for prop.
61 | init(param: Int) {
62 | prop = param
63 | }
64 |
65 | /// Returns a string value concatenating `param1` and `param2`.
66 | ///
67 | /// - Parameters:
68 | /// - param1: An Int value for prefix.
69 | /// - param2: A String value for suffix.
70 | ///
71 | /// - Returns: A string concatenating given params.
72 | func bar(param1: Int, param2: String) -> String {
73 | return "\(param1)" + param2
74 | }
75 | }
76 | ```
77 |
78 | ## [Developer's Certificate of Origin 1.1](https://elinux.org/Developer_Certificate_Of_Origin)
79 | By making a contribution to this project, I certify that:
80 |
81 | (a) The contribution was created in whole or in part by me and I
82 | have the right to submit it under the open source license
83 | indicated in the file; or
84 |
85 | (b) The contribution is based upon previous work that, to the best
86 | of my knowledge, is covered under an appropriate open source
87 | license and I have the right under that license to submit that
88 | work with modifications, whether created in whole or in part
89 | by me, under the same open source license (unless I am
90 | permitted to submit under a different license), as indicated
91 | in the file; or
92 |
93 | (c) The contribution was provided directly to me by some other
94 | person who certified (a), (b) or (c) and I have not modified
95 | it.
96 |
97 | (d) I understand and agree that this project and the contribution
98 | are public and that a record of the contribution (including all
99 | personal information I submit with it, including my sign-off) is
100 | maintained indefinitely and may be redistributed consistent with
101 | this project or the open source license(s) involved.
102 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Form/FormTextFieldContent.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------