Add NavigationItem host component (#27)

Resolves #22

* Add StackControllerItem host component
* Move {Stack,Navigation}Presenter, related changes
* Update StandardComponents doc with new name
* Fix styling after view controller behavior change
* Remove style from StackView.Props.init
* Add `viewDidLoad` comment to UIViewComponent
* Fix tests not compiling
* Implement basic UIHostComponent for NavigationItem
* Implement prefersLargeTitles, test item titles
* Remove unused public func isSubtypeOf<T, U, V, W>
* Remove unused NavigationItemBox, fix formatting
* Remove missing file from the project
This commit is contained in:
Max Desiatov 2019-02-11 08:24:51 +00:00 committed by GitHub
parent dfe658fe5b
commit e413b06683
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 305 additions and 113 deletions

View File

@ -8,7 +8,7 @@
import Gluon
struct StackModal: PureLeafComponent {
struct NavigationModal: PureLeafComponent {
struct Props: Equatable {
let isPresented: State<Bool>
}
@ -16,9 +16,10 @@ struct StackModal: PureLeafComponent {
static func render(props: Props) -> AnyNode {
return props.isPresented.value ?
ModalPresenter.node(
StackPresenter<NavRouter>.node(
NavigationPresenter<NavRouter>.node(
.init(
initial: .first,
prefersLargeTitles: true,
routerProps: .init(
onPress: Handler { props.isPresented.set(false) }
)
@ -44,11 +45,15 @@ struct SimpleModal: LeafComponent {
let backgroundColor = hooks.state(0)
return props.isPresented.value ? ModalPresenter.node(
View.node(.init(Style(backgroundColor: colors[backgroundColor.value].0)),
View.node(
.init(Style(
backgroundColor: colors[backgroundColor.value].0,
Edges.equal(to: .parent)
)),
StackView.node(.init(
axis: .vertical,
distribution: .fillEqually,
Style(Edges.equal(to: .parent))
Edges.equal(to: .parent)
), [
Button.node(.init(
onPress: Handler { props.isPresented.set(false) }
@ -58,7 +63,8 @@ struct SimpleModal: LeafComponent {
valueHandler: Handler(backgroundColor.set)),
colors.map { $0.1 }
),
]))
])
)
) : Null.node()
}
}
@ -78,12 +84,15 @@ struct TableModal: PureLeafComponent {
}
static func render(props: Props) -> AnyNode {
let list = ListView<ListProvider>.node(.init(singleSection: [1, 2, 3]))
let list = ListView<ListProvider>.node(.init(
singleSection: [1, 2, 3],
Style(Edges.equal(to: .parent))
))
return props.isPresented.value ? ModalPresenter.node(list) : Null.node()
}
}
struct ConstrainModal: LeafComponent {
struct ConstraintModal: LeafComponent {
struct Props: Equatable {
let isPresented: State<Bool>
}
@ -93,11 +102,11 @@ struct ConstrainModal: LeafComponent {
return props.isPresented.value ? ModalPresenter.node(
View.node(
.init(Style(backgroundColor: .white)),
.init(Style(backgroundColor: .white, Edges.equal(to: .parent))),
StackView.node(.init(
axis: .vertical,
distribution: .fillEqually,
Style(Edges.equal(to: .parent))
Edges.equal(to: .parent)
), [
Button.node(.init(
onPress: Handler { props.isPresented.set(false) }
@ -136,11 +145,11 @@ struct DatePickerModal: LeafComponent {
let formattedDate = dateFormatter.string(from: date.value)
return props.isPresented.value ? ModalPresenter.node(
View.node(
.init(Style(backgroundColor: .white)),
.init(Style(backgroundColor: .white, Edges.equal(to: .parent))),
StackView.node(.init(
axis: .vertical,
distribution: .fillEqually,
Style(Edges.equal(to: .parent))
Edges.equal(to: .parent)
), [
Button.node(.init(
onPress: Handler { props.isPresented.set(false) }
@ -180,11 +189,11 @@ struct CALayerModal: LeafComponent {
return props.isPresented.value ? ModalPresenter.node(
View.node(
.init(Style(backgroundColor: .white)),
.init(Style(backgroundColor: .white, Edges.equal(to: .parent))),
StackView.node(.init(
axis: .vertical,
distribution: .fillEqually,
Style(Edges.equal(to: .parent))
Edges.equal(to: .parent)
), [
Button.node(.init(
onPress: Handler { props.isPresented.set(false) }
@ -202,7 +211,7 @@ struct CALayerModal: LeafComponent {
borderWidth: Float(state.value * 10),
cornerRadius: Float(state.value * 10),
opacity: Float(state.value),
Width.equal(to: .parent, constant: 10)
Width.equal(to: .parent)
)
),
Label.node(.init(
@ -231,7 +240,7 @@ struct Counter: LeafComponent {
let isStackModalPresented = hooks.state(false)
let isAnimationModalPresented = hooks.state(false)
let isTableModalPresented = hooks.state(false)
let isConstrainModalPresented = hooks.state(false)
let isConstraintModalPresented = hooks.state(false)
let isDatePickerModalPresented = hooks.state(false)
let isCALayerModalPresented = hooks.state(false)
let switchState = hooks.state(true)
@ -264,7 +273,7 @@ struct Counter: LeafComponent {
isEnabled: isEnabled.value,
onPress: Handler { isStackModalPresented.set(true) }
),
"Present Stack Modal"
"Present Navigation Modal"
),
Button.node(
@ -286,9 +295,9 @@ struct Counter: LeafComponent {
Button.node(
.init(
isEnabled: isEnabled.value,
onPress: Handler { isConstrainModalPresented.set(true) }
onPress: Handler { isConstraintModalPresented.set(true) }
),
"Present Constrain Modal"
"Present Constraint Modal"
),
Button.node(
@ -307,13 +316,13 @@ struct Counter: LeafComponent {
"Present Core Animation Layer Modal"
),
StackModal.node(.init(isPresented: isStackModalPresented)),
NavigationModal.node(.init(isPresented: isStackModalPresented)),
SimpleModal.node(.init(isPresented: isAnimationModalPresented)),
TableModal.node(.init(isPresented: isTableModalPresented)),
ConstrainModal.node(.init(isPresented: isConstrainModalPresented)),
ConstraintModal.node(.init(isPresented: isConstraintModalPresented)),
DatePickerModal.node(.init(isPresented: isDatePickerModalPresented)),
@ -388,7 +397,7 @@ struct Counter: LeafComponent {
alignment: .center,
axis: .vertical,
distribution: .fillEqually,
Style(Edges.equal(to: .parent))
Edges.equal(to: .parent)
),
children
)

View File

@ -8,7 +8,7 @@
import Gluon
struct NavRouter: StackRouter {
struct NavRouter: NavigationRouter {
enum Route {
case first
case second
@ -32,32 +32,35 @@ struct NavRouter: StackRouter {
), "Close Modal")
switch route {
case .first:
return View.node(
.init(Style(backgroundColor: .white)), [
return
NavigationItem.node(
.init(title: "First", titleMode: .standard),
View.node(
.init(Style(backgroundColor: .white, Edges.equal(to: .parent))), [
close,
Label.node(.init(
alignment: .center,
Style(Rectangle(Point(x: 0, y: 200),
Size(width: 200, height: 200)))
), "first"),
Button.node(.init(
onPress: Handler { push(.second) },
Style(Rectangle(Point(x: 0, y: 400),
Size(width: 200, height: 200)))
), "second"),
), "Go to Second"),
]
)
)
case .second:
return View.node(
.init(Style(backgroundColor: .white)), [
return
NavigationItem.node(
.init(title: "Second", titleMode: .large),
View.node(
.init(Style(backgroundColor: .white, Edges.equal(to: .parent))), [
close,
Label.node(.init(
alignment: .center,
Style(Rectangle(Point(x: 0, y: 200),
Size(width: 200, height: 200)))
), "second"),
), "This is second"),
]
)
)
}
}
}

View File

@ -19,4 +19,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 506176d9f57e313b696258608414e29b871b813e
COCOAPODS: 1.6.0.rc.2
COCOAPODS: 1.6.0

View File

@ -47,7 +47,7 @@
D1C3A72421F0DE5300C6B884 /* Insets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C3A72221F0DC6300C6B884 /* Insets.swift */; };
D1CB7D2921E136010075C0C3 /* SegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CB7D2821E136010075C0C3 /* SegmentedControl.swift */; };
D1CB7D2B21E136520075C0C3 /* SegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CB7D2A21E136520075C0C3 /* SegmentedControl.swift */; };
D1CB7D2E21E29CC80075C0C3 /* StackController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CB7D2D21E29CC80075C0C3 /* StackController.swift */; };
D1CB7D2E21E29CC80075C0C3 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CB7D2D21E29CC80075C0C3 /* NavigationController.swift */; };
D1E8755521EF2ED200D4A7BA /* HooksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E8755421EF2ED200D4A7BA /* HooksTests.swift */; };
D1EC92F021F5B8000026C207 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EC92EF21F5B8000026C207 /* ListView.swift */; };
D1EF77522205C579008829EC /* Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EF77512205C579008829EC /* Constraint.swift */; };
@ -72,12 +72,15 @@
D1EF777B220744FC008829EC /* FirstBaseline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EF777A220744FB008829EC /* FirstBaseline.swift */; };
D1EF777D220745AB008829EC /* BaselineConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EF777C220745AB008829EC /* BaselineConstraint.swift */; };
D1EF777F220745F4008829EC /* LastBaseline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EF777E220745F4008829EC /* LastBaseline.swift */; };
D1EF77812208AEF5008829EC /* NavigationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EF77802208AEF5008829EC /* NavigationItem.swift */; };
D1EF77852208BB10008829EC /* TabItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EF77842208BB10008829EC /* TabItem.swift */; };
D1EF77862208BB13008829EC /* NavigationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EF77822208BAF6008829EC /* NavigationItem.swift */; };
OBJ_100 /* Animated.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_48 /* Animated.swift */; };
OBJ_101 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_49 /* Button.swift */; };
OBJ_102 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_50 /* Label.swift */; };
OBJ_104 /* ModalPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_53 /* ModalPresenter.swift */; };
OBJ_105 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_54 /* Router.swift */; };
OBJ_106 /* StackPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_55 /* StackPresenter.swift */; };
OBJ_106 /* NavigationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_55 /* NavigationPresenter.swift */; };
OBJ_107 /* TabPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_56 /* TabPresenter.swift */; };
OBJ_108 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_58 /* Color.swift */; };
OBJ_109 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_59 /* Event.swift */; };
@ -115,7 +118,7 @@
OBJ_174 /* UIValueComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* UIValueComponent.swift */; };
OBJ_175 /* UIViewComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* UIViewComponent.swift */; };
OBJ_176 /* Slider.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* Slider.swift */; };
OBJ_177 /* StackController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_29 /* StackController.swift */; };
OBJ_177 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_29 /* NavigationController.swift */; };
OBJ_178 /* StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_30 /* StackView.swift */; };
OBJ_179 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_31 /* View.swift */; };
OBJ_180 /* ContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* ContainerViewController.swift */; };
@ -198,7 +201,7 @@
D1C3A72221F0DC6300C6B884 /* Insets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Insets.swift; sourceTree = "<group>"; };
D1CB7D2821E136010075C0C3 /* SegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControl.swift; sourceTree = "<group>"; };
D1CB7D2A21E136520075C0C3 /* SegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControl.swift; sourceTree = "<group>"; };
D1CB7D2D21E29CC80075C0C3 /* StackController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackController.swift; sourceTree = "<group>"; };
D1CB7D2D21E29CC80075C0C3 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
D1E8755421EF2ED200D4A7BA /* HooksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HooksTests.swift; sourceTree = "<group>"; };
D1EC92EF21F5B8000026C207 /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = "<group>"; };
D1EF77512205C579008829EC /* Constraint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constraint.swift; sourceTree = "<group>"; };
@ -223,6 +226,9 @@
D1EF777A220744FB008829EC /* FirstBaseline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstBaseline.swift; sourceTree = "<group>"; };
D1EF777C220745AB008829EC /* BaselineConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaselineConstraint.swift; sourceTree = "<group>"; };
D1EF777E220745F4008829EC /* LastBaseline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastBaseline.swift; sourceTree = "<group>"; };
D1EF77802208AEF5008829EC /* NavigationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItem.swift; sourceTree = "<group>"; };
D1EF77822208BAF6008829EC /* NavigationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItem.swift; sourceTree = "<group>"; };
D1EF77842208BB10008829EC /* TabItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItem.swift; sourceTree = "<group>"; };
"Gluon::Gluon::Product" /* Gluon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Gluon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"Gluon::GluonTestRenderer::Product" /* GluonTestRenderer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = GluonTestRenderer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"Gluon::GluonTests::Product" /* GluonTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = GluonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -240,7 +246,7 @@
OBJ_26 /* UIValueComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIValueComponent.swift; sourceTree = "<group>"; };
OBJ_27 /* UIViewComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewComponent.swift; sourceTree = "<group>"; };
OBJ_28 /* Slider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Slider.swift; sourceTree = "<group>"; };
OBJ_29 /* StackController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackController.swift; sourceTree = "<group>"; };
OBJ_29 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
OBJ_30 /* StackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackView.swift; sourceTree = "<group>"; };
OBJ_31 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
OBJ_33 /* ContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerViewController.swift; sourceTree = "<group>"; };
@ -259,7 +265,7 @@
OBJ_50 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = "<group>"; };
OBJ_53 /* ModalPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalPresenter.swift; sourceTree = "<group>"; };
OBJ_54 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
OBJ_55 /* StackPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackPresenter.swift; sourceTree = "<group>"; };
OBJ_55 /* NavigationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationPresenter.swift; sourceTree = "<group>"; };
OBJ_56 /* TabPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPresenter.swift; sourceTree = "<group>"; };
OBJ_58 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
OBJ_59 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = "<group>"; };
@ -357,7 +363,7 @@
D1EC92EF21F5B8000026C207 /* ListView.swift */,
D1CB7D2821E136010075C0C3 /* SegmentedControl.swift */,
OBJ_64 /* Slider.swift */,
D1CB7D2D21E29CC80075C0C3 /* StackController.swift */,
D1CB7D2D21E29CC80075C0C3 /* NavigationController.swift */,
OBJ_65 /* StackView.swift */,
OBJ_66 /* View.swift */,
A62891F921F5DB02001BE090 /* Switch.swift */,
@ -375,7 +381,8 @@
OBJ_17 /* ModalPresenter.swift */,
D1CB7D2A21E136520075C0C3 /* SegmentedControl.swift */,
OBJ_28 /* Slider.swift */,
OBJ_29 /* StackController.swift */,
OBJ_29 /* NavigationController.swift */,
D1EF77802208AEF5008829EC /* NavigationItem.swift */,
OBJ_30 /* StackView.swift */,
OBJ_31 /* View.swift */,
A62891F721F5DAEE001BE090 /* Switch.swift */,
@ -526,8 +533,10 @@
children = (
OBJ_53 /* ModalPresenter.swift */,
OBJ_54 /* Router.swift */,
OBJ_55 /* StackPresenter.swift */,
OBJ_55 /* NavigationPresenter.swift */,
OBJ_56 /* TabPresenter.swift */,
D1EF77822208BAF6008829EC /* NavigationItem.swift */,
D1EF77842208BB10008829EC /* TabItem.swift */,
);
path = Presenters;
sourceTree = "<group>";
@ -802,11 +811,12 @@
D1EF777D220745AB008829EC /* BaselineConstraint.swift in Sources */,
A633562521FB96EE00BCC525 /* CenterX.swift in Sources */,
OBJ_176 /* Slider.swift in Sources */,
OBJ_177 /* StackController.swift in Sources */,
OBJ_177 /* NavigationController.swift in Sources */,
A633562121FB5C8800BCC525 /* Leading.swift in Sources */,
OBJ_178 /* StackView.swift in Sources */,
D163F9D621F27F8C00BA464B /* GluonViewController.swift in Sources */,
D16FCB7C21FCB3870033C5C0 /* TableViewBox.swift in Sources */,
D1EF77812208AEF5008829EC /* NavigationItem.swift in Sources */,
OBJ_179 /* View.swift in Sources */,
OBJ_180 /* ContainerViewController.swift in Sources */,
OBJ_181 /* ControlBox.swift in Sources */,
@ -850,13 +860,13 @@
D1EF775C2205C64D008829EC /* Bottom.swift in Sources */,
D1EF776C2205C727008829EC /* LastBaseline.swift in Sources */,
D1EF77642205C6C8008829EC /* Trailing.swift in Sources */,
OBJ_106 /* StackPresenter.swift in Sources */,
OBJ_106 /* NavigationPresenter.swift in Sources */,
D1EF776A2205C717008829EC /* FirstBaseline.swift in Sources */,
OBJ_107 /* TabPresenter.swift in Sources */,
D1BDBBE621E0F07800FBBCDF /* MountedNull.swift in Sources */,
OBJ_108 /* Color.swift in Sources */,
OBJ_109 /* Event.swift in Sources */,
D1CB7D2E21E29CC80075C0C3 /* StackController.swift in Sources */,
D1CB7D2E21E29CC80075C0C3 /* NavigationController.swift in Sources */,
OBJ_110 /* Rectangle.swift in Sources */,
OBJ_111 /* Second.swift in Sources */,
OBJ_112 /* Style.swift in Sources */,
@ -870,6 +880,7 @@
D16FCB8121FDD2550033C5C0 /* Weak.swift in Sources */,
D1EF77542205C583008829EC /* Edges.swift in Sources */,
OBJ_117 /* MountedComponent.swift in Sources */,
D1EF77852208BB10008829EC /* TabItem.swift in Sources */,
OBJ_118 /* MountedCompositeComponent.swift in Sources */,
OBJ_119 /* MountedHostComponent.swift in Sources */,
D1EF775A2205C639008829EC /* Top.swift in Sources */,
@ -880,6 +891,7 @@
OBJ_123 /* Store.swift in Sources */,
OBJ_124 /* Unique.swift in Sources */,
D1EF77682205C6F6008829EC /* CenterY.swift in Sources */,
D1EF77862208BB13008829EC /* NavigationItem.swift in Sources */,
A62891FA21F5DB02001BE090 /* Switch.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -1,16 +1,17 @@
//
// StackController.swift
// NavigationController.swift
// Gluon
//
// Created by Max Desiatov on 06/01/2019.
//
public struct StackController: HostComponent {
public struct NavigationController: HostComponent {
public typealias Children = [AnyNode]
public struct Props: Equatable {
public let hidesBarsWhenKeyboardAppears: Bool?
public let popAnimated: Bool
public let prefersLargeTitles: Bool
public let pushAnimated: Bool
public let onPop: Handler<()>
}

View File

@ -35,18 +35,50 @@ public struct StackView: HostComponent {
public let alignment: Alignment
public let axis: Axis
public let distribution: Distribution
public let style: Style?
public let spacing: Double
public init(alignment: Alignment = .fill,
/// not exposing style: UIStackView is a non-rendering subclass of UIView
/// https://useyourloaf.com/blog/stack-view-background-color/
public let style: Style?
public init(
alignment: Alignment = .fill,
axis: Axis = .horizontal,
distribution: Distribution = .fill,
spacing: Double = 0,
_ style: Style? = nil) {
_ frame: Rectangle
) {
self.alignment = alignment
self.axis = axis
self.distribution = distribution
self.style = style
style = Style(frame)
self.spacing = spacing
}
public init(
alignment: Alignment = .fill,
axis: Axis = .horizontal,
distribution: Distribution = .fill,
spacing: Double = 0,
_ constraint: Constraint
) {
self.alignment = alignment
self.axis = axis
self.distribution = distribution
style = Style(constraint)
self.spacing = spacing
}
public init(
alignment: Alignment = .fill,
axis: Axis = .horizontal,
distribution: Distribution = .fill,
spacing: Double = 0
) {
self.alignment = alignment
self.axis = axis
self.distribution = distribution
style = nil
self.spacing = spacing
}
}

View File

@ -0,0 +1,27 @@
//
// NavigationItem.swift
// GluonUIKit
//
// Created by Max Desiatov on 04/02/2019.
//
public struct NavigationItem: HostComponent {
public struct Props: Equatable {
/// The mode to use when displaying the title of the navigation bar.
public enum TitleMode {
case automatic
case large
case standard
}
public let title: String?
public let titleMode: TitleMode
public init(title: String? = nil, titleMode: TitleMode = .automatic) {
self.title = title
self.titleMode = titleMode
}
}
public typealias Children = AnyNode
}

View File

@ -1,11 +1,11 @@
//
// StackPresenter.swift
// NavigationPresenter.swift
// Gluon
//
// Created by Max Desiatov on 31/12/2018.
//
public protocol StackRouter: Router {
public protocol NavigationRouter: Router {
static func route(
props: Props,
route: Route,
@ -15,11 +15,12 @@ public protocol StackRouter: Router {
) -> AnyNode
}
public struct StackPresenter<T: StackRouter>: LeafComponent {
public struct NavigationPresenter<T: NavigationRouter>: LeafComponent {
public struct Props: Equatable {
public let hidesBarsWhenKeyboardAppears: Bool?
public let initial: T.Route
public let popAnimated: Bool
public let prefersLargeTitles: Bool
public let pushAnimated: Bool
public let routerProps: T.Props
@ -27,12 +28,14 @@ public struct StackPresenter<T: StackRouter>: LeafComponent {
hidesBarsWhenKeyboardAppears: Bool? = nil,
initial: T.Route,
popAnimated: Bool = true,
prefersLargeTitles: Bool = false,
pushAnimated: Bool = true,
routerProps: T.Props
) {
self.hidesBarsWhenKeyboardAppears = hidesBarsWhenKeyboardAppears
self.initial = initial
self.popAnimated = popAnimated
self.prefersLargeTitles = prefersLargeTitles
self.pushAnimated = pushAnimated
self.routerProps = routerProps
}
@ -43,10 +46,11 @@ public struct StackPresenter<T: StackRouter>: LeafComponent {
let pop = { stack.set(Array(stack.value.dropLast())) }
return StackController.node(
return NavigationController.node(
.init(
hidesBarsWhenKeyboardAppears: props.hidesBarsWhenKeyboardAppears,
popAnimated: props.popAnimated,
prefersLargeTitles: props.prefersLargeTitles,
pushAnimated: props.pushAnimated,
onPop: Handler(pop)
),
@ -63,16 +67,18 @@ public struct StackPresenter<T: StackRouter>: LeafComponent {
}
}
extension StackPresenter.Props where T.Props == Null {
extension NavigationPresenter.Props where T.Props == Null {
public init(
hidesBarsWhenKeyboardAppears: Bool? = nil,
initial: T.Route,
popAnimated: Bool = true,
prefersLargeTitles: Bool = false,
pushAnimated: Bool = true
) {
self.hidesBarsWhenKeyboardAppears = hidesBarsWhenKeyboardAppears
self.initial = initial
self.popAnimated = popAnimated
self.prefersLargeTitles = prefersLargeTitles
self.pushAnimated = pushAnimated
routerProps = Null()
}

View File

@ -0,0 +1,14 @@
//
// TabItem.swift
// Gluon
//
// Created by Max Desiatov on 04/02/2019.
//
public struct TabItem: Equatable {
public let title: String?
public init(title: String? = nil) {
self.title = title
}
}

View File

@ -9,16 +9,14 @@
components by `StackReconciler`.
*/
public final class MountedHostComponent<R: Renderer>: MountedComponent<R> {
// FIXME: we probably can avoid making this class public
private var managedChildren = [Weak<R.Target>: MountedComponent<R>]()
private var mountedChildren = [MountedComponent<R>]()
/** Target of a closest ancestor host component. As a parent of this component
might not be a host component, but a composite component, we need to pass
around the target of a host component to its closests descendent host
comoponents. Thus, a parent target is not the same as a target of a parent
component. */
comoponents. Thus, a parent target is not always the same as a target of
a parent component. */
private let parentTarget: R.Target
/** Target of this host component supplied by a renderer after mounting has

View File

@ -21,7 +21,7 @@ final class ContainerViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = contained
override func viewDidLoad() {
view.addSubview(contained)
}
}

View File

@ -95,10 +95,11 @@ final class TableViewBox<T: CellProvider>: ViewBox<GluonTableView> {
return dataSource.props
}
set {
if dataSource.props.model != newValue.model {
defer { view.reloadData() }
}
let oldModel = dataSource.props.model
dataSource.props = newValue
if oldModel != newValue.model {
view.reloadData()
}
}
}

View File

@ -16,7 +16,9 @@ extension ModalPresenter: UIHostComponent {
}
static func update(target: UITarget,
node: AnyNode) {}
node: AnyNode) {
// FIXME: update presentation-related props on the target here
}
static func unmount(target: UITarget) {
target.viewController.dismiss(animated: true)

View File

@ -28,7 +28,7 @@ final class GluonNavigationController: UINavigationController {
}
}
extension StackController: UIHostComponent {
extension NavigationController: UIHostComponent {
static func mountTarget(to parent: UITarget,
component: UIKitRenderer.MountedHost,
_: UIKitRenderer) -> UITarget? {
@ -44,6 +44,11 @@ extension StackController: UIHostComponent {
result.containerViewController.hidesBarsWhenKeyboardAppears = $0
}
if #available(iOS 11.0, *) {
result.containerViewController.navigationBar.prefersLargeTitles =
props.prefersLargeTitles
}
switch parent {
// FIXME: this `case` handler is duplicated with `UIViewComponent`,
// should this be generalised as a protocol?

View File

@ -0,0 +1,73 @@
//
// StackControllerItem.swift
// GluonUIKit
//
// Created by Max Desiatov on 04/02/2019.
//
import Gluon
import UIKit
extension UINavigationItem.LargeTitleDisplayMode {
init(mode: NavigationItem.Props.TitleMode) {
switch mode {
case .automatic:
self = .automatic
case .large:
self = .always
case .standard:
self = .never
}
}
}
extension NavigationItem: UIHostComponent {
static func mountTarget(to parent: UITarget,
component: UIKitRenderer.MountedHost,
_: UIKitRenderer) -> UITarget? {
guard
let parent = parent as? ViewControllerBox<GluonNavigationController>
else {
parentAssertionFailure()
return nil
}
guard
let parentProps = parent.node?.props.value as? NavigationController.Props
else {
propsAssertionFailure()
return nil
}
let viewController = UIViewController()
let result = ViewControllerBox(viewController, component.node)
update(target: result, node: component.node)
parent.containerViewController.pushViewController(
viewController,
animated: parentProps.pushAnimated
)
return result
}
static func update(target: UITarget, node: AnyNode) {
guard let target = target as? ViewControllerBox<UIViewController> else {
targetAssertionFailure()
return
}
guard let props = node.props.value as? NavigationItem.Props else {
propsAssertionFailure()
return
}
let item = target.viewController.navigationItem
item.title = props.title
if #available(iOS 11.0, *) {
item.largeTitleDisplayMode = .init(mode: props.titleMode)
}
}
static func unmount(target: UITarget) {}
}

View File

@ -31,7 +31,9 @@ private func applyStyle<T: UIView, P: StyleProps>(_ target: ViewBox<T>,
let view = target.view
style.allowsEdgeAntialiasing.flatMap { view.layer.allowsEdgeAntialiasing = $0 }
style.allowsEdgeAntialiasing.flatMap {
view.layer.allowsEdgeAntialiasing = $0
}
style.allowsGroupOpacity.flatMap { view.layer.allowsGroupOpacity = $0 }
style.alpha.flatMap { view.alpha = CGFloat($0) }
style.backgroundColor.flatMap { view.backgroundColor = UIColor($0) }
@ -95,18 +97,19 @@ extension UIViewComponent where Target == Target.DefaultValue,
let result: ViewBox<Target>
let parentRequiresViewController = parent.node?.isSubtypeOf(
ModalPresenter.self, or: StackController.self, or: AnyTabPresenter.self
ModalPresenter.self,
or: NavigationController.self,
or: AnyTabPresenter.self
) ?? false
// UIViewController parent target can't present a bare `ViewBox` target,
// it needs to be wrapped with `ContainerViewController` first.
if parentRequiresViewController {
result = box(
for: target,
ContainerViewController(contained: target),
component,
renderer
)
let container = ContainerViewController(contained: target)
// trigger `viewDidLoad` on `ContainerViewController` for safe constraints
// installation
container.loadViewIfNeeded()
result = box(for: target, container, component, renderer)
} else {
result = box(for: target, parent.viewController, component, renderer)
}
@ -123,9 +126,9 @@ extension UIViewComponent where Target == Target.DefaultValue,
case let box as ViewBox<GluonTableCell>:
box.view.addSubview(target)
case let box as ViewControllerBox<GluonNavigationController>
where parent.node?.isSubtypeOf(StackController.self) ?? false:
where parent.node?.isSubtypeOf(NavigationController.self) ?? false:
guard let props = parent.node?.props.value
as? StackController.Props else {
as? NavigationController.Props else {
propsAssertionFailure()
return nil
}
@ -136,7 +139,9 @@ extension UIViewComponent where Target == Target.DefaultValue,
)
case let box as ViewControllerBox<UIViewController>
where parent.node?.isSubtypeOf(ModalPresenter.self) ?? false:
guard let props = parent.node?.props.value as? ModalPresenter.Props else {
guard
let props = parent.node?.props.value as? ModalPresenter.Props
else {
propsAssertionFailure()
return nil
}
@ -144,6 +149,9 @@ extension UIViewComponent where Target == Target.DefaultValue,
box.viewController.present(result.viewController,
animated: props.presentAnimated,
completion: nil)
case let box as ViewControllerBox<UIViewController>
where parent.node?.isSubtypeOf(NavigationItem.self) ?? false:
box.viewController.view.addSubview(target)
default:
parentAssertionFailure()
}

View File

@ -12,9 +12,10 @@ import UIKit
// compiler bug
let _modalPresenterWitnessTableHack: UIHostComponent.Type = ModalPresenter.self
let _stackControllerWitnessTableHack: UIHostComponent.Type =
StackController.self
let _listViewWitnessTableHack: UIHostComponent.Type = ListView<HackyProvider>.self
NavigationController.self
let _navigationItemWitnessTableHack: UIHostComponent.Type = NavigationItem.self
let _listViewWitnessTableHack: UIHostComponent.Type =
ListView<HackyProvider>.self
struct HackyProvider: SimpleCellProvider {
static func cell(

View File

@ -33,7 +33,7 @@ struct Counter: LeafComponent {
return StackView.node(.init(axis: .vertical,
distribution: .fillEqually,
Style(Edges.equal(to: .parent))),
Edges.equal(to: .parent)),
children)
}
}

View File

@ -17,7 +17,7 @@
| Gluon Component | Rendered on iOS as |
|---|---|
| [`ModalPresenter`](https://github.com/MaxDesiatov/Gluon/blob/master/Sources/Gluon/Components/Presenters/ModalPresenter.swift) | [`UIViewController`](https://developer.apple.com/documentation/uikit/uiviewcontroller) with modal presentation|
| [`StackPresenter`](https://github.com/MaxDesiatov/Gluon/blob/master/Sources/Gluon/Components/Presenters/StackPresenter.swift) | [`UINavigationController`](https://developer.apple.com/documentation/uikit/uinavigationcontroller) |
| [`NavigationPresenter`](https://github.com/MaxDesiatov/Gluon/blob/master/Sources/Gluon/Components/Presenters/NavigationPresenter.swift) | [`UINavigationController`](https://developer.apple.com/documentation/uikit/uinavigationcontroller) |
| [`TabPresenter`](https://github.com/MaxDesiatov/Gluon/blob/master/Sources/Gluon/Components/Presenters/TabPresenter.swift) | [`UITabBarController`](https://developer.apple.com/documentation/uikit/uitabbarcontroller) |
## Style