diff --git a/Few-iOS/Button.swift b/Few-iOS/Button.swift index c451a03..f053dfe 100644 --- a/Few-iOS/Button.swift +++ b/Few-iOS/Button.swift @@ -8,13 +8,6 @@ import UIKit -extension UIControlState: Hashable { - static let all: [UIControlState] = [.Normal, .Selected, .Disabled, .Highlighted] - public var hashValue: Int { - return Int(rawValue) - } -} - public class Button: Element { public var attributedTitles: [UIControlState: NSAttributedString] public var images: [UIControlState: UIImage] @@ -23,6 +16,8 @@ public class Button: Element { public var selected: Bool public var highlighted: Bool + private static let layoutButton = UIButton() + private var trampoline = TargetActionTrampoline() public convenience init(attributedTitle: NSAttributedString = NSAttributedString(), image: UIImage? = nil, action: (() -> Void) = { }) { @@ -43,7 +38,37 @@ public class Button: Element { self.enabled = enabled self.highlighted = highlighted trampoline.action = action - super.init(frame: CGRect(x: 0, y: 0, width: 50, height: 23)) + super.init() + } + + public var controlState: UIControlState { + return UIControlState(enabled: enabled, selected: selected, highlighted: highlighted) + } + + public override func assembleLayoutNode() -> Node { + let childNodes = children.map { $0.assembleLayoutNode() } + + return Node(size: frame.size, children: childNodes, direction: direction, margin: marginWithPlatformSpecificAdjustments, padding: paddingWithPlatformSpecificAdjustments, wrap: wrap, justification: justification, selfAlignment: selfAlignment, childAlignment: childAlignment, flex: flex) { w in + let controlState = self.controlState + + let layoutButton = Button.layoutButton + layoutButton.enabled = self.enabled + layoutButton.highlighted = self.highlighted + layoutButton.selected = self.selected + + let attributedTitle = self.attributedTitles[controlState] ?? self.attributedTitles[.Normal] + if layoutButton.attributedTitleForState(controlState) != attributedTitle { + layoutButton.setAttributedTitle(attributedTitle, forState: controlState) + } + + let image = self.images[controlState] ?? self.images[.Normal] + if image != layoutButton.imageForState(controlState) { + layoutButton.setImage(image, forState: controlState) + } + + let fittingSize = CGSize(width: w.isNaN ? ABigDimension : w, height: ABigDimension) + return layoutButton.sizeThatFits(fittingSize) + } } // MARK: Element diff --git a/Few-iOS/Label.swift b/Few-iOS/Label.swift index 5040c83..90e1195 100644 --- a/Few-iOS/Label.swift +++ b/Few-iOS/Label.swift @@ -10,7 +10,7 @@ import UIKit private let DefaultLabelFont = UIFont.systemFontOfSize(UIFont.systemFontSize()) -private let ABigDimension: CGFloat = 10000 +internal let ABigDimension: CGFloat = 10000 private let sizingLabel = UILabel() diff --git a/Few-iOS/Switch.swift b/Few-iOS/Switch.swift new file mode 100644 index 0000000..d228d8b --- /dev/null +++ b/Few-iOS/Switch.swift @@ -0,0 +1,74 @@ + +import UIKit + +public class Switch: Element { + static let intrinsicSize = UISwitch().intrinsicContentSize() + + public init(on: Bool, enabled: Bool = true, animatesOnSetting: Bool = true, onTintColor: UIColor? = nil, tintColor: UIColor? = nil, thumbTintColor: UIColor? = nil, action: (Bool -> Void)? = nil) { + let initialFrame = CGRect(origin: .zeroPoint, size: Switch.intrinsicSize) + self.on = on + self.onTintColor = onTintColor + self.thumbTintColor = thumbTintColor + self.tintColor = tintColor + self.animatesOnSetting = animatesOnSetting + self.enabled = enabled + self.action = action + super.init(frame: initialFrame) + if let action = action { + trampoline.action = { aSwitch in action(aSwitch.on) } + } + } + + private var trampoline = TargetActionTrampolineWithSender() + public var on: Bool + public var enabled: Bool + public var action: (Bool -> Void)? + public var animatesOnSetting: Bool + + public var onTintColor: UIColor? + public var tintColor: UIColor? + public var thumbTintColor: UIColor? + + public override func applyDiff(old: Element, realizedSelf: RealizedElement?) { + super.applyDiff(old, realizedSelf: realizedSelf) + + if let view = realizedSelf?.view as? UISwitch { + if let oldSwitch = old as? Switch { + let newTrampoline = oldSwitch.trampoline + newTrampoline.action = trampoline.action // Make sure the newest action is used + trampoline = newTrampoline + } + + if enabled != view.enabled { + view.enabled = enabled + } + + if on != view.on { + view.setOn(on, animated: animatesOnSetting) + } + + if view.onTintColor != onTintColor { + view.onTintColor = onTintColor + } + + if view.tintColor != tintColor { + view.tintColor = tintColor + } + + if view.thumbTintColor != thumbTintColor { + view.thumbTintColor = thumbTintColor + } + } + } + + public override func createView() -> ViewType? { + let view = UISwitch() + view.on = on + view.enabled = enabled + view.onTintColor = onTintColor + view.tintColor = tintColor + view.thumbTintColor = thumbTintColor + view.addTarget(trampoline.target, action: trampoline.selector, forControlEvents: .ValueChanged) + return view + } +} diff --git a/Few-iOS/TableView.swift b/Few-iOS/TableView.swift index f321fd4..de5c1bf 100644 --- a/Few-iOS/TableView.swift +++ b/Few-iOS/TableView.swift @@ -251,8 +251,10 @@ public class TableView: Element { private let sectionFooters: [Element?] private let header: Element? private let footer: Element? + private let contentInset: UIEdgeInsets + private let scrollIndicatorInsets: UIEdgeInsets - public init(_ elements: [[Element]], sectionHeaders: [Element?] = [], sectionFooters: [Element?] = [], header: Element? = nil, footer: Element? = nil, selectedRow: NSIndexPath? = nil, selectionChanged: (NSIndexPath -> ())? = nil) { + public init(_ elements: [[Element]], sectionHeaders: [Element?] = [], sectionFooters: [Element?] = [], header: Element? = nil, footer: Element? = nil, selectedRow: NSIndexPath? = nil, contentInset: UIEdgeInsets = .zeroInsets, scrollIndicatorInsets: UIEdgeInsets = .zeroInsets, selectionChanged: (NSIndexPath -> ())? = nil) { self.elements = elements self.selectionChanged = selectionChanged self.selectedRow = selectedRow @@ -260,6 +262,8 @@ public class TableView: Element { self.sectionFooters = sectionFooters self.header = header self.footer = footer + self.contentInset = contentInset + self.scrollIndicatorInsets = scrollIndicatorInsets } // MARK: - @@ -295,7 +299,14 @@ public class TableView: Element { } else if tableView.tableFooterView == handler.footerView { tableView.tableFooterView = nil } - + + if contentInset != tableView.contentInset { + tableView.contentInset = contentInset + } + + if scrollIndicatorInsets != tableView.scrollIndicatorInsets { + tableView.scrollIndicatorInsets = scrollIndicatorInsets + } } } @@ -307,6 +318,8 @@ public class TableView: Element { tableView.handler?.selectionChanged = selectionChanged tableView.alpha = alpha tableView.hidden = hidden + tableView.contentInset = contentInset + tableView.scrollIndicatorInsets = scrollIndicatorInsets if let header = header { handler.headerView.updateWithElement(header) let layout = header.assembleLayoutNode().layout(maxWidth: tableView.frame.width) diff --git a/Few-iOS/UIControlStateExtensions.swift b/Few-iOS/UIControlStateExtensions.swift new file mode 100644 index 0000000..143e142 --- /dev/null +++ b/Few-iOS/UIControlStateExtensions.swift @@ -0,0 +1,35 @@ + +import UIKit + +extension UIControlState: Hashable { + static let all: Set = { + var states = Set() + for enabled in [false, true] { + for selected in [false, true] { + for highlighted in [false, true] { + let state = UIControlState(enabled: enabled, selected: selected, highlighted: highlighted) + states.insert(state) + } + } + } + return states + }() + + public var hashValue: Int { + return Int(rawValue) + } + + init(enabled: Bool, selected: Bool, highlighted: Bool) { + var result = UIControlState.Normal + if !enabled { + result |= .Disabled + } + if selected { + result |= .Selected + } + if highlighted { + result |= .Highlighted + } + self = result + } +} diff --git a/Few-iOS/UIGeometryExtensions.swift b/Few-iOS/UIGeometryExtensions.swift new file mode 100644 index 0000000..8bdba7d --- /dev/null +++ b/Few-iOS/UIGeometryExtensions.swift @@ -0,0 +1,10 @@ + +import UIKit + +extension UIEdgeInsets: Equatable { + static let zeroInsets = UIEdgeInsetsZero +} + +public func ==(inset0: UIEdgeInsets, inset1: UIEdgeInsets) -> Bool { + return UIEdgeInsetsEqualToEdgeInsets(inset0, inset1) +} diff --git a/Few.xcodeproj/project.pbxproj b/Few.xcodeproj/project.pbxproj index 48a941a..25a088d 100644 --- a/Few.xcodeproj/project.pbxproj +++ b/Few.xcodeproj/project.pbxproj @@ -70,6 +70,11 @@ CC2C508B1AFECB0600019685 /* SwiftBox.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = CC2C507D1AFEC95900019685 /* SwiftBox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CC2C508C1AFECB0B00019685 /* SwiftBox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC2C507A1AFEC93D00019685 /* SwiftBox.framework */; }; CC2C508D1AFECB0B00019685 /* SwiftBox.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CC2C507A1AFEC93D00019685 /* SwiftBox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CC8A32361B54AEB700626C4D /* Switch.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC8A32351B54AEB700626C4D /* Switch.swift */; }; + CC95B32B1B54BD8B00F7D83C /* AttributedStringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC95B32A1B54BD8B00F7D83C /* AttributedStringExtensions.swift */; }; + CC95B32C1B54BD8B00F7D83C /* AttributedStringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC95B32A1B54BD8B00F7D83C /* AttributedStringExtensions.swift */; }; + CC95B32E1B54BE3C00F7D83C /* UIControlStateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC95B32D1B54BE3C00F7D83C /* UIControlStateExtensions.swift */; }; + CC95B3311B54C1D100F7D83C /* UIGeometryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC95B3301B54C1D100F7D83C /* UIGeometryExtensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -190,6 +195,10 @@ CC2C507D1AFEC95900019685 /* SwiftBox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftBox.framework; path = Carthage/Build/Mac/SwiftBox.framework; sourceTree = ""; }; CC2C50801AFEC99700019685 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/Mac/Quick.framework; sourceTree = SOURCE_ROOT; }; CC2C50841AFECA8100019685 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/Mac/Nimble.framework; sourceTree = SOURCE_ROOT; }; + CC8A32351B54AEB700626C4D /* Switch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Switch.swift; sourceTree = ""; }; + CC95B32A1B54BD8B00F7D83C /* AttributedStringExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringExtensions.swift; sourceTree = ""; }; + CC95B32D1B54BE3C00F7D83C /* UIControlStateExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIControlStateExtensions.swift; sourceTree = ""; }; + CC95B3301B54C1D100F7D83C /* UIGeometryExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIGeometryExtensions.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -243,9 +252,11 @@ 888E59AE1A4684850062A36B /* Few-iOS */ = { isa = PBXGroup; children = ( + CC95B32F1B54BE4400F7D83C /* Extensions */, 888E59CC1A4684AF0062A36B /* iOS.swift */, A2FA89121AB33C1F00561FBF /* View.swift */, A2FA89141AB33E0800561FBF /* Button.swift */, + CC8A32351B54AEB700626C4D /* Switch.swift */, A2FA89161AB33EDA00561FBF /* Label.swift */, A2B9C7471AB387F7002648DA /* TableView.swift */, A2B9C74F1AB390B3002648DA /* Image.swift */, @@ -319,6 +330,7 @@ 8897689419AED8AF00DD079B /* Util.swift */, 8897689319AED8AF00DD079B /* TargetActionTrampoline.swift */, 888E59DA1A4687E10062A36B /* QuickLook.swift */, + CC95B32A1B54BD8B00F7D83C /* AttributedStringExtensions.swift */, ); path = FewCore; sourceTree = ""; @@ -412,6 +424,15 @@ name = "Frameworks-Mac"; sourceTree = ""; }; + CC95B32F1B54BE4400F7D83C /* Extensions */ = { + isa = PBXGroup; + children = ( + CC95B32D1B54BE3C00F7D83C /* UIControlStateExtensions.swift */, + CC95B3301B54C1D100F7D83C /* UIGeometryExtensions.swift */, + ); + name = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -624,14 +645,18 @@ buildActionMask = 2147483647; files = ( A2B9C7541AB43D0A002648DA /* Input.swift in Sources */, + CC95B32E1B54BE3C00F7D83C /* UIControlStateExtensions.swift in Sources */, 888E59D81A4687330062A36B /* Util.swift in Sources */, 888E59CF1A4687330062A36B /* Element.swift in Sources */, + CC95B32C1B54BD8B00F7D83C /* AttributedStringExtensions.swift in Sources */, A2FA89171AB33EDA00561FBF /* Label.swift in Sources */, A2FA89131AB33C1F00561FBF /* View.swift in Sources */, + CC95B3311B54C1D100F7D83C /* UIGeometryExtensions.swift in Sources */, 888E59D01A4687330062A36B /* Component.swift in Sources */, 888E59D91A4687330062A36B /* TargetActionTrampoline.swift in Sources */, 888E59CD1A4684AF0062A36B /* iOS.swift in Sources */, A2B9C7501AB390B3002648DA /* Image.swift in Sources */, + CC8A32361B54AEB700626C4D /* Switch.swift in Sources */, 888E59D41A4687330062A36B /* Empty.swift in Sources */, A2B9C7481AB387F7002648DA /* TableView.swift in Sources */, 889FE1BE1A4E11F000D53F3E /* QuickLook.swift in Sources */, @@ -648,6 +673,7 @@ 88995D6D1A855D2000526313 /* View.swift in Sources */, 883CECA81A4532A600B8A510 /* Password.swift in Sources */, 889768A019AED8AF00DD079B /* Util.swift in Sources */, + CC95B32B1B54BD8B00F7D83C /* AttributedStringExtensions.swift in Sources */, 8897689619AED8AF00DD079B /* Button.swift in Sources */, 8897689919AED8AF00DD079B /* Element.swift in Sources */, 8897689C19AED8AF00DD079B /* Label.swift in Sources */, diff --git a/FewCore/AttributedStringExtensions.swift b/FewCore/AttributedStringExtensions.swift new file mode 100644 index 0000000..2a9c382 --- /dev/null +++ b/FewCore/AttributedStringExtensions.swift @@ -0,0 +1,8 @@ + +import Foundation + +extension NSAttributedString: Equatable { } + +public func ==(str0: NSAttributedString, str1: NSAttributedString) -> Bool { + return str0.isEqualToAttributedString(str1) +} \ No newline at end of file diff --git a/FewDemo-iOS/AppDelegate.swift b/FewDemo-iOS/AppDelegate.swift index a385f14..663aff0 100644 --- a/FewDemo-iOS/AppDelegate.swift +++ b/FewDemo-iOS/AppDelegate.swift @@ -27,12 +27,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window = UIWindow(frame: UIScreen.mainScreen().bounds) let vc = UIViewController() + vc.title = "Few Demo" + vc.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .Plain, target: self, action: "nextTapped") + let nav = UINavigationController(rootViewController: vc) vc.view.backgroundColor = UIColor.whiteColor() appComponent.addToView(vc.view) - window?.rootViewController = vc + window?.rootViewController = nav window?.makeKeyAndVisible() return true } + + @objc func nextTapped() { + appComponent.updateState { (var state) in + switch state.activeComponent { + case .TableView: + state.activeComponent = .Counter + case .Counter: + state.activeComponent = .Input + case .Input: + state.activeComponent = .TableView + } + return state + } + } } - diff --git a/FewDemo-iOS/ViewController.swift b/FewDemo-iOS/ViewController.swift index 7589161..b16353b 100644 --- a/FewDemo-iOS/ViewController.swift +++ b/FewDemo-iOS/ViewController.swift @@ -69,6 +69,7 @@ private func renderRow2(row: Int) -> Element { struct TableViewDemoState { var headerHeight: CGFloat var footerHeight: CGFloat + var headerSwitchIsOn: Bool } func renderTableView(component: Component, state: TableViewDemoState) -> Element { @@ -79,27 +80,41 @@ func renderTableView(component: Component, state: TableViewD return renderRow2(rowNum) } } + let headerTitle = "Increase Header Height" + (state.headerSwitchIsOn ? " (on)" : " (off)") return TableView([elements], sectionHeaders: [Label("Section Header!")], - header: Button(attributedTitle: NSAttributedString(string: "Increase Header Height"), - action: { + header: Element(direction: .Row, + justification: Justification.SpaceAround, + childAlignment: ChildAlignment.Center, + children: [ + Button(attributedTitle: NSAttributedString(string: headerTitle), + action: { + component.updateState { (var state) in + state.headerHeight += 10 + return state + } + }), + Switch(on: state.headerSwitchIsOn, enabled: true, animatesOnSetting: true, onTintColor: UIColor.purpleColor(), action: { on in component.updateState { (var state) in - state.headerHeight += 10 + state.headerSwitchIsOn = on return state } - }).height(state.headerHeight).width(200), + }) + ]).height(state.headerHeight).width(UIScreen.mainScreen().bounds.width), footer: Button(attributedTitle: NSAttributedString(string: "Increase Footer Height"), action: { component.updateState { (var state) in state.footerHeight += 10 return state } - }).height(state.footerHeight).width(200), + }).height(state.footerHeight).width(UIScreen.mainScreen().bounds.width), sectionFooters: [Label("Section Footer!")], + contentInset: UIEdgeInsetsMake(64, 0, 0, 0), + scrollIndicatorInsets: UIEdgeInsetsMake(64, 0, 0, 0), selectionChanged: println) .flex(1) } -let TableViewDemo = { Component(initialState: TableViewDemoState(headerHeight: 60, footerHeight: 60), render: renderTableView) } +let TableViewDemo = { Component(initialState: TableViewDemoState(headerHeight: 60, footerHeight: 60, headerSwitchIsOn: true), render: renderTableView) } func renderInput(component: Component, state: String) -> Element { return Element() @@ -123,25 +138,21 @@ func renderInput(component: Component, state: String) -> Element { let InputDemo = { Component(initialState: "", render: renderInput) } struct AppState { + enum ContentComponent { + case TableView + case Counter + case Input + } + let tableViewComponent: Component let counterComponent: Component let inputComponent: Component - var activeComponent: ActiveComponent - - mutating func updateActiveComponent(newComponent: ActiveComponent) -> AppState { - activeComponent = newComponent - return self - } -} -enum ActiveComponent { - case TableView - case Counter - case Input + var activeComponent: ContentComponent } func renderApp(component: Component, state: AppState) -> Element { - var contentComponent: Element! + let contentComponent: Element switch state.activeComponent { case .TableView: contentComponent = state.tableViewComponent @@ -151,30 +162,11 @@ func renderApp(component: Component, state: AppState) -> Element { contentComponent = state.inputComponent } - let showMore = { component.updateState(toggleDisplay) } - return Element() - .direction(.Column) - .children([ - Element() - .children([ - contentComponent.flex(1) - ]) - .flex(1), - Button(attributedTitle: NSAttributedString(string: "Show me more!"), action: showMore) - .width(200) - .margin(Edges(uniform: 10)) - .selfAlignment(.Center) + return Element( + flex: 1, + children: [ + contentComponent.flex(1) ]) } -func toggleDisplay(var state: AppState) -> AppState { - switch state.activeComponent { - case .TableView: - return state.updateActiveComponent(.Counter) - case .Counter: - return state.updateActiveComponent(.Input) - case .Input: - return state.updateActiveComponent(.TableView) - } -}