From 97f714e166e7df1f0f78134e76a6686579e14056 Mon Sep 17 00:00:00 2001 From: Raul Riera Date: Wed, 3 Apr 2019 10:55:38 -0400 Subject: [PATCH 1/2] Open source simple type cells --- FunctionalTableData.xcodeproj/project.pbxproj | 113 ++++++++++++++- .../xcschemes/FunctionalTableData.xcscheme | 2 +- .../Cells/Button/ButtonState.swift | 35 +++++ .../Cells/Combined/CombinedState.swift | 38 ++++++ .../Cells/Combined/CombinedView.swift | 37 +++++ .../Cells/ControlText.swift | 129 ++++++++++++++++++ .../Cells/ImageState.swift | 78 +++++++++++ .../Cells/Label/LabelState.swift | 41 ++++++ FunctionalTableDataDemo/Cells/LabelCell.swift | 39 ------ .../Cells/Spacer/SpacerState.swift | 29 ++++ .../Cells/Spacer/SpacerView.swift | 22 +++ .../Cells/Subtitle/SubtitleState.swift | 40 ++++++ .../Cells/Subtitle/SubtitleView.swift | 47 +++++++ .../Cells/Switch/SwitchState.swift | 45 ++++++ .../Cells/UIControl+Extensions.swift | 83 +++++++++++ .../CollectionExampleController.swift | 6 +- .../TableExampleController.swift | 10 +- 17 files changed, 739 insertions(+), 55 deletions(-) create mode 100644 FunctionalTableDataDemo/Cells/Button/ButtonState.swift create mode 100644 FunctionalTableDataDemo/Cells/Combined/CombinedState.swift create mode 100644 FunctionalTableDataDemo/Cells/Combined/CombinedView.swift create mode 100644 FunctionalTableDataDemo/Cells/ControlText.swift create mode 100644 FunctionalTableDataDemo/Cells/ImageState.swift create mode 100644 FunctionalTableDataDemo/Cells/Label/LabelState.swift delete mode 100644 FunctionalTableDataDemo/Cells/LabelCell.swift create mode 100644 FunctionalTableDataDemo/Cells/Spacer/SpacerState.swift create mode 100644 FunctionalTableDataDemo/Cells/Spacer/SpacerView.swift create mode 100644 FunctionalTableDataDemo/Cells/Subtitle/SubtitleState.swift create mode 100644 FunctionalTableDataDemo/Cells/Subtitle/SubtitleView.swift create mode 100644 FunctionalTableDataDemo/Cells/Switch/SwitchState.swift create mode 100644 FunctionalTableDataDemo/Cells/UIControl+Extensions.swift diff --git a/FunctionalTableData.xcodeproj/project.pbxproj b/FunctionalTableData.xcodeproj/project.pbxproj index 3a0a1be..0e07170 100644 --- a/FunctionalTableData.xcodeproj/project.pbxproj +++ b/FunctionalTableData.xcodeproj/project.pbxproj @@ -15,9 +15,20 @@ 17E57FF5208A404800BFCC3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17E57FF3208A404800BFCC3D /* LaunchScreen.storyboard */; }; 17E57FFA208A415900BFCC3D /* FunctionalTableData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C7A26FC1F2FA0F800360E9B /* FunctionalTableData.framework */; }; 17E57FFB208A415900BFCC3D /* FunctionalTableData.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4C7A26FC1F2FA0F800360E9B /* FunctionalTableData.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 17E58000208A425F00BFCC3D /* LabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E57FFF208A425F00BFCC3D /* LabelCell.swift */; }; 3624340420D2F40100A75787 /* Array+TableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3624340320D2F40100A75787 /* Array+TableSection.swift */; }; 36C9208D20D3EB7500DA4251 /* TableSectionsValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C9208C20D3EB7500DA4251 /* TableSectionsValidationTests.swift */; }; + 4C1A850B2254FDC900066633 /* SpacerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A84FA2254FDC900066633 /* SpacerState.swift */; }; + 4C1A850C2254FDC900066633 /* SpacerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A84FB2254FDC900066633 /* SpacerView.swift */; }; + 4C1A850D2254FDC900066633 /* LabelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A84FD2254FDC900066633 /* LabelState.swift */; }; + 4C1A850E2254FDC900066633 /* ImageState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A84FE2254FDC900066633 /* ImageState.swift */; }; + 4C1A850F2254FDC900066633 /* CombinedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A85002254FDC900066633 /* CombinedState.swift */; }; + 4C1A85102254FDC900066633 /* CombinedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A85012254FDC900066633 /* CombinedView.swift */; }; + 4C1A85112254FDC900066633 /* SubtitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A85032254FDC900066633 /* SubtitleView.swift */; }; + 4C1A85122254FDC900066633 /* SubtitleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A85042254FDC900066633 /* SubtitleState.swift */; }; + 4C1A85132254FDC900066633 /* SwitchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A85062254FDC900066633 /* SwitchState.swift */; }; + 4C1A85142254FDC900066633 /* UIControl+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A85072254FDC900066633 /* UIControl+Extensions.swift */; }; + 4C1A85152254FDC900066633 /* ControlText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A85082254FDC900066633 /* ControlText.swift */; }; + 4C1A85162254FDC900066633 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A850A2254FDC900066633 /* ButtonState.swift */; }; 4C63250B1F8AA89B00B2B74B /* TableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCCE8441F8AA7CD00C73258 /* TableCell.swift */; }; 4C63250C1F8AA89D00B2B74B /* TableItemConfigType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCCE8451F8AA7CD00C73258 /* TableItemConfigType.swift */; }; 4C63250D1F8AA8A000B2B74B /* UITableView+Reusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCCE8461F8AA7CD00C73258 /* UITableView+Reusable.swift */; }; @@ -95,9 +106,20 @@ 17E57FF1208A404800BFCC3D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 17E57FF4208A404800BFCC3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 17E57FF6208A404800BFCC3D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 17E57FFF208A425F00BFCC3D /* LabelCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelCell.swift; sourceTree = ""; }; 3624340320D2F40100A75787 /* Array+TableSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+TableSection.swift"; sourceTree = ""; }; 36C9208C20D3EB7500DA4251 /* TableSectionsValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableSectionsValidationTests.swift; sourceTree = ""; }; + 4C1A84FA2254FDC900066633 /* SpacerState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerState.swift; sourceTree = ""; }; + 4C1A84FB2254FDC900066633 /* SpacerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerView.swift; sourceTree = ""; }; + 4C1A84FD2254FDC900066633 /* LabelState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelState.swift; sourceTree = ""; }; + 4C1A84FE2254FDC900066633 /* ImageState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageState.swift; path = ../ImageState.swift; sourceTree = ""; }; + 4C1A85002254FDC900066633 /* CombinedState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombinedState.swift; sourceTree = ""; }; + 4C1A85012254FDC900066633 /* CombinedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombinedView.swift; sourceTree = ""; }; + 4C1A85032254FDC900066633 /* SubtitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubtitleView.swift; sourceTree = ""; }; + 4C1A85042254FDC900066633 /* SubtitleState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubtitleState.swift; sourceTree = ""; }; + 4C1A85062254FDC900066633 /* SwitchState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchState.swift; sourceTree = ""; }; + 4C1A85072254FDC900066633 /* UIControl+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIControl+Extensions.swift"; sourceTree = ""; }; + 4C1A85082254FDC900066633 /* ControlText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlText.swift; sourceTree = ""; }; + 4C1A850A2254FDC900066633 /* ButtonState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonState.swift; sourceTree = ""; }; 4C7A26FC1F2FA0F800360E9B /* FunctionalTableData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FunctionalTableData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4C7A26FF1F2FA0F800360E9B /* FunctionalTableData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FunctionalTableData.h; sourceTree = ""; }; 4C7A27001F2FA0F800360E9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -177,6 +199,65 @@ path = FunctionalTableDataDemo; sourceTree = ""; }; + 4C1A84F92254FDC900066633 /* Spacer */ = { + isa = PBXGroup; + children = ( + 4C1A84FA2254FDC900066633 /* SpacerState.swift */, + 4C1A84FB2254FDC900066633 /* SpacerView.swift */, + ); + path = Spacer; + sourceTree = ""; + }; + 4C1A84FC2254FDC900066633 /* Label */ = { + isa = PBXGroup; + children = ( + 4C1A84FD2254FDC900066633 /* LabelState.swift */, + ); + path = Label; + sourceTree = ""; + }; + 4C1A84FF2254FDC900066633 /* Combined */ = { + isa = PBXGroup; + children = ( + 4C1A85002254FDC900066633 /* CombinedState.swift */, + 4C1A85012254FDC900066633 /* CombinedView.swift */, + ); + path = Combined; + sourceTree = ""; + }; + 4C1A85022254FDC900066633 /* Subtitle */ = { + isa = PBXGroup; + children = ( + 4C1A85032254FDC900066633 /* SubtitleView.swift */, + 4C1A85042254FDC900066633 /* SubtitleState.swift */, + ); + path = Subtitle; + sourceTree = ""; + }; + 4C1A85052254FDC900066633 /* Switch */ = { + isa = PBXGroup; + children = ( + 4C1A85062254FDC900066633 /* SwitchState.swift */, + ); + path = Switch; + sourceTree = ""; + }; + 4C1A85092254FDC900066633 /* Button */ = { + isa = PBXGroup; + children = ( + 4C1A850A2254FDC900066633 /* ButtonState.swift */, + ); + path = Button; + sourceTree = ""; + }; + 4C1A85172255017A00066633 /* Image */ = { + isa = PBXGroup; + children = ( + 4C1A84FE2254FDC900066633 /* ImageState.swift */, + ); + path = Image; + sourceTree = ""; + }; 4C7A26F21F2FA0F800360E9B = { isa = PBXGroup; children = ( @@ -294,7 +375,15 @@ 4CCFDFF520B2230E00584343 /* Cells */ = { isa = PBXGroup; children = ( - 17E57FFF208A425F00BFCC3D /* LabelCell.swift */, + 4C1A85092254FDC900066633 /* Button */, + 4C1A84FF2254FDC900066633 /* Combined */, + 4C1A85172255017A00066633 /* Image */, + 4C1A84FC2254FDC900066633 /* Label */, + 4C1A84F92254FDC900066633 /* Spacer */, + 4C1A85022254FDC900066633 /* Subtitle */, + 4C1A85052254FDC900066633 /* Switch */, + 4C1A85082254FDC900066633 /* ControlText.swift */, + 4C1A85072254FDC900066633 /* UIControl+Extensions.swift */, ); path = Cells; sourceTree = ""; @@ -384,7 +473,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0930; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = Shopify; TargetAttributes = { 17E57FE5208A404700BFCC3D = { @@ -406,10 +495,9 @@ }; buildConfigurationList = 4C7A26F61F2FA0F800360E9B /* Build configuration list for PBXProject "FunctionalTableData" */; compatibilityVersion = "Xcode 8.0"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( - English, en, Base, ); @@ -457,10 +545,21 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C1A850F2254FDC900066633 /* CombinedState.swift in Sources */, + 4C1A850D2254FDC900066633 /* LabelState.swift in Sources */, + 4C1A85102254FDC900066633 /* CombinedView.swift in Sources */, 17E57FED208A404700BFCC3D /* CollectionExampleController.swift in Sources */, + 4C1A85122254FDC900066633 /* SubtitleState.swift in Sources */, 17E57FE9208A404700BFCC3D /* AppDelegate.swift in Sources */, - 17E58000208A425F00BFCC3D /* LabelCell.swift in Sources */, + 4C1A85152254FDC900066633 /* ControlText.swift in Sources */, 17E57FEB208A404700BFCC3D /* TableExampleController.swift in Sources */, + 4C1A85132254FDC900066633 /* SwitchState.swift in Sources */, + 4C1A85112254FDC900066633 /* SubtitleView.swift in Sources */, + 4C1A85142254FDC900066633 /* UIControl+Extensions.swift in Sources */, + 4C1A85162254FDC900066633 /* ButtonState.swift in Sources */, + 4C1A850C2254FDC900066633 /* SpacerView.swift in Sources */, + 4C1A850E2254FDC900066633 /* ImageState.swift in Sources */, + 4C1A850B2254FDC900066633 /* SpacerState.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FunctionalTableData.xcodeproj/xcshareddata/xcschemes/FunctionalTableData.xcscheme b/FunctionalTableData.xcodeproj/xcshareddata/xcschemes/FunctionalTableData.xcscheme index 08c5109..808036c 100644 --- a/FunctionalTableData.xcodeproj/xcshareddata/xcschemes/FunctionalTableData.xcscheme +++ b/FunctionalTableData.xcodeproj/xcshareddata/xcschemes/FunctionalTableData.xcscheme @@ -1,6 +1,6 @@ + +public struct ButtonState: Equatable { + public let title: String + public let isEnabled: Bool + public let action: (UIButton) -> Void + + public static func updateView(_ view: UIButton, state: ButtonState?) { + guard let state = state else { + view.setTitle(nil, for: .normal) + view.isEnabled = true + view.setActions([]) + return + } + + view.setTitle(state.title, for: .normal) + view.isEnabled = state.isEnabled + view.setAction(for: .touchUpInside, action: state.action) + } + + public static func ==(lhs: ButtonState, rhs: ButtonState) -> Bool { + return lhs.title == rhs.title && lhs.isEnabled == rhs.isEnabled + } +} diff --git a/FunctionalTableDataDemo/Cells/Combined/CombinedState.swift b/FunctionalTableDataDemo/Cells/Combined/CombinedState.swift new file mode 100644 index 0000000..60d3015 --- /dev/null +++ b/FunctionalTableDataDemo/Cells/Combined/CombinedState.swift @@ -0,0 +1,38 @@ +// +// CombinedState.swift +// Shopify +// +// Created by Geoffrey Foster on 2017-01-18. +// Copyright © 2017 Shopify. All rights reserved. +// + +import UIKit +import FunctionalTableData + +public typealias CombinedCell = HostCell, CombinedState, Layout> + +public struct CombinedState: Equatable { + public let state1: S1 + public let state2: S2 + public init(state1: S1, state2: S2) { + self.state1 = state1 + self.state2 = state2 + } + + public static func ==(lhs: CombinedState, rhs: CombinedState) -> Bool { + return lhs.state1 == rhs.state1 && lhs.state2 == rhs.state2 + } +} + +extension CombinedState: Encodable where S1: Encodable, S2: Encodable { + enum CodingKeys: CodingKey { + case state1 + case state2 + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(state1, forKey: .state1) + try container.encode(state2, forKey: .state2) + } +} diff --git a/FunctionalTableDataDemo/Cells/Combined/CombinedView.swift b/FunctionalTableDataDemo/Cells/Combined/CombinedView.swift new file mode 100644 index 0000000..96497f0 --- /dev/null +++ b/FunctionalTableDataDemo/Cells/Combined/CombinedView.swift @@ -0,0 +1,37 @@ +// +// CombinedView.swift +// Shopify +// +// Created by Geoffrey Foster on 2017-01-18. +// Copyright © 2017 Shopify. All rights reserved. +// + +import UIKit +import FunctionalTableData + +public class CombinedView: UIView { + public let view1 = View1() + public let view2 = View2() + public let stackView: UIStackView + + public override init(frame: CGRect) { + stackView = UIStackView(frame: frame) + super.init(frame: frame) + stackView.addArrangedSubview(view1) + stackView.addArrangedSubview(view2) + + stackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor) + ]) + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/FunctionalTableDataDemo/Cells/ControlText.swift b/FunctionalTableDataDemo/Cells/ControlText.swift new file mode 100644 index 0000000..f005c55 --- /dev/null +++ b/FunctionalTableDataDemo/Cells/ControlText.swift @@ -0,0 +1,129 @@ +// +// ControlText.swift +// Shopify +// +// Created by Geoffrey Foster on 2016-05-19. +// Copyright © 2016 Shopify. All rights reserved. +// + +import UIKit + +public enum TruncationStyle: String, Encodable { + case truncate + case multiline +} + +public enum ControlText: Encodable { + case plain(String) + case attributed(NSAttributedString) + + public func encode(to encoder: Encoder) throws { + switch self { + case .plain(let string): + try "plain: \(string)".encode(to: encoder) + case .attributed(let attributedString): + try "attributed: \(attributedString.string)".encode(to: encoder) + } + } +} + +extension ControlText: Equatable { + public static func ==(lhs: ControlText, rhs: ControlText) -> Bool { + if case .plain(let value1) = lhs, case .plain(let value2) = rhs { + return value1 == value2 + } else if case .attributed(let value1) = lhs, case .attributed(let value2) = rhs { + return value1 == value2 + } + return false + } +} + +public extension ControlText { + var plainText: String { + switch self { + case .plain(let value): + return value + case .attributed(let value): + return value.string + } + } + + var attributedText: NSAttributedString { + switch self { + case .plain(let value): + return NSAttributedString(string: value) + case .attributed(let value): + return value + } + } +} + +public extension UILabel { + func setControlText(_ controlText: ControlText?) { + if let controlText = controlText { + switch controlText { + case .plain(let value): + self.text = value + case .attributed(let value): + self.attributedText = value + } + } else { + self.text = nil + } + } + + func apply(truncationStyle: TruncationStyle) { + switch truncationStyle { + case .truncate: + lineBreakMode = .byTruncatingTail + numberOfLines = 1 + case .multiline: + lineBreakMode = .byWordWrapping + numberOfLines = 0 + } + } +} + +public extension UITextField { + func setControlText(_ controlText: ControlText?) { + if let controlText = controlText { + switch controlText { + case .plain(let value): + self.text = value + case .attributed(let value): + self.attributedText = value + } + } else { + self.text = nil + } + } + + func setControlTextPlaceholder(_ controlText: ControlText?) { + if let controlText = controlText { + switch controlText { + case .plain(let value): + self.placeholder = value + case .attributed(let value): + self.attributedPlaceholder = value + } + } else { + self.placeholder = nil + } + } +} + +public extension UIButton { + func setControlText(_ controlText: ControlText?, forState state: UIControl.State) { + if let controlText = controlText { + switch controlText { + case .plain(let value): + setTitle(value, for: state) + case .attributed(let value): + setAttributedTitle(value, for: state) + } + } else { + setTitle(nil, for: state) + setAttributedTitle(nil, for: state) + } + } +} diff --git a/FunctionalTableDataDemo/Cells/ImageState.swift b/FunctionalTableDataDemo/Cells/ImageState.swift new file mode 100644 index 0000000..1aec114 --- /dev/null +++ b/FunctionalTableDataDemo/Cells/ImageState.swift @@ -0,0 +1,78 @@ +// +// ImageState.swift +// Shopify +// +// Created by Raul Riera on 2017-10-13. +// Copyright © 2017 Shopify. All rights reserved. +// + +import UIKit +import FunctionalTableData + +public typealias ImageCell = HostCell +public typealias ImageWithSubtitleCell = CombinedCell + +public protocol ImageLoadable { + func setImage(from: URL, placeholder: UIImage?) + func cancelLoadingImage() +} + +public struct ImageState: Equatable { + private static let widthAnchorIdentifier = "ImageState.Width" + private static let heightAnchorIdentifier = "ImageState.Height" + + let image: UIImage? + let url: URL? + let width: CGFloat? + let height: CGFloat? + + public init(image: UIImage, width: CGFloat?, height: CGFloat?) { + self.image = image + self.url = nil + self.width = width + self.height = height + } + + public init(url: URL, width: CGFloat?, height: CGFloat?) { + self.image = nil + self.url = url + self.width = width + self.height = height + } + + static func updateView(_ view: UIImageView & ImageLoadable, state: ImageState?) { + guard let state = state else { + view.cancelLoadingImage() + view.image = nil + + return + } + + let constraints = view.constraints.filter { $0.identifier == ImageState.widthAnchorIdentifier || $0.identifier == ImageState.heightAnchorIdentifier } + constraints.forEach { $0.isActive = false } + + if let width = state.width { + let widthConstraint = view.widthAnchor.constraint(equalToConstant: width) + widthConstraint.isActive = true + widthConstraint.identifier = ImageState.widthAnchorIdentifier + } + + if let height = state.height { + let heightConstraint = view.heightAnchor.constraint(equalToConstant: height) + heightConstraint.isActive = true + heightConstraint.identifier = ImageState.heightAnchorIdentifier + } + + view.contentMode = .scaleAspectFit + + if let url = state.url { + view.setImage(from: url, placeholder: nil) + } else { + view.image = state.image + } + } + + public static func ==(lhs: ImageState, rhs: ImageState) -> Bool { + return lhs.url == rhs.url && lhs.width == rhs.width && lhs.height == rhs.height + } +} diff --git a/FunctionalTableDataDemo/Cells/Label/LabelState.swift b/FunctionalTableDataDemo/Cells/Label/LabelState.swift new file mode 100644 index 0000000..14c102e --- /dev/null +++ b/FunctionalTableDataDemo/Cells/Label/LabelState.swift @@ -0,0 +1,41 @@ +// +// LabelViewState.swift +// Shopify +// +// Created by Geoffrey Foster on 2016-10-30. +// Copyright © 2016 Shopify. All rights reserved. +// + +import UIKit +import FunctionalTableData + +public typealias LabelCell = HostCell + +public struct LabelState: Equatable { + let text: ControlText + let font: UIFont? + let textColor: UIColor? + let truncationStyle: TruncationStyle + let textAlignment: NSTextAlignment + + public init(text: ControlText, font: UIFont? = nil, textColor: UIColor? = nil, truncationStyle: TruncationStyle = .truncate, textAlignment: NSTextAlignment = .natural) { + self.text = text + self.font = font + self.textColor = textColor + self.truncationStyle = truncationStyle + self.textAlignment = textAlignment + } + + public static func updateView(_ view: UILabel, state: LabelState?) { + guard let state = state else { + // reset parts of state that need to be reset + view.setControlText(nil) + return + } + view.font = state.font + view.textColor = state.textColor + view.apply(truncationStyle: state.truncationStyle) + view.setControlText(state.text) + view.textAlignment = state.textAlignment + } +} diff --git a/FunctionalTableDataDemo/Cells/LabelCell.swift b/FunctionalTableDataDemo/Cells/LabelCell.swift deleted file mode 100644 index d010f33..0000000 --- a/FunctionalTableDataDemo/Cells/LabelCell.swift +++ /dev/null @@ -1,39 +0,0 @@ -import UIKit -import FunctionalTableData - -public typealias LabelCell = HostCell - -/// A very simple state for a `UILabel` allowing a quick configuration of its text, font, and color values. -public struct LabelState: Equatable { - public let text: String - public let font: UIFont - public let color: UIColor - - public init(text: String, font: UIFont = UIFont.systemFont(ofSize: 17), color: UIColor = .black) { - self.text = text - self.font = font - self.color = color - } - - /// Update the view with the contents of the state. - /// - /// - Parameters: - /// - view: `UIView` that responds to this state. - /// - state: data to update the view with. If `nil` the view is being reused by the tableview. - public static func updateView(_ view: UILabel, state: LabelState?) { - guard let state = state else { - view.text = nil - view.font = UIFont.systemFont(ofSize: 17) - view.textColor = .black - return - } - - view.text = state.text - view.font = state.font - view.textColor = state.color - } - - public static func ==(lhs: LabelState, rhs: LabelState) -> Bool { - return lhs.text == rhs.text && lhs.font == rhs.font && lhs.color == rhs.color - } -} diff --git a/FunctionalTableDataDemo/Cells/Spacer/SpacerState.swift b/FunctionalTableDataDemo/Cells/Spacer/SpacerState.swift new file mode 100644 index 0000000..0c4d3fc --- /dev/null +++ b/FunctionalTableDataDemo/Cells/Spacer/SpacerState.swift @@ -0,0 +1,29 @@ +// +// SpacerCell.swift +// Shopify +// +// Created by Raul Riera on 2017-12-18. +// Copyright © 2017 Raul Riera. All rights reserved. +// + +import UIKit +import FunctionalTableData + +public typealias SpacerCell = HostCell, EdgeBasedTableItemLayout> + +public struct SpacerState: Equatable { + public let height: CGFloat + + public init(height: CGFloat = 12) { + self.height = height + } + + public static func updateView(_ view: View, state: SpacerState?) { + view.backgroundColor = UIColor.clear + view.height = state?.height ?? 12 + } + + public static func ==(lhs: SpacerState, rhs: SpacerState) -> Bool { + return lhs.height == rhs.height + } +} diff --git a/FunctionalTableDataDemo/Cells/Spacer/SpacerView.swift b/FunctionalTableDataDemo/Cells/Spacer/SpacerView.swift new file mode 100644 index 0000000..518e0d7 --- /dev/null +++ b/FunctionalTableDataDemo/Cells/Spacer/SpacerView.swift @@ -0,0 +1,22 @@ +// +// SpacerCell.swift +// Shopify +// +// Created by Raul Riera on 2017-12-18. +// Copyright © 2017 Raul Riera. All rights reserved. +// + +import UIKit +import FunctionalTableData + +public class SpacerView: UIView { + public var height: CGFloat = 12 { + didSet { + invalidateIntrinsicContentSize() + } + } + + public override var intrinsicContentSize: CGSize { + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } +} diff --git a/FunctionalTableDataDemo/Cells/Subtitle/SubtitleState.swift b/FunctionalTableDataDemo/Cells/Subtitle/SubtitleState.swift new file mode 100644 index 0000000..b3f0810 --- /dev/null +++ b/FunctionalTableDataDemo/Cells/Subtitle/SubtitleState.swift @@ -0,0 +1,40 @@ +// +// SubtitleCell.swift +// Shopify +// +// Created by Raul Riera on 2017-09-21. +// Copyright © 2017 Shopify. All rights reserved. +// + +import UIKit +import FunctionalTableData + +public typealias SubtitleCell = HostCell + +public struct SubtitleState: Equatable { + let title: ControlText + let titleTruncationStyle: TruncationStyle + let subtitle: ControlText + let subtitleTruncationStyle: TruncationStyle + + public init(title: ControlText, titleTruncationStyle: TruncationStyle = .truncate, subtitle: ControlText, subtitleTruncationStyle: TruncationStyle = .multiline) { + self.title = title + self.titleTruncationStyle = titleTruncationStyle + self.subtitle = subtitle + self.subtitleTruncationStyle = subtitleTruncationStyle + } + + public static func updateView(_ view: SubtitleView, state: SubtitleState?) { + guard let state = state else { + // reset parts of state that need to be reset + view.titleLabel.setControlText(nil) + view.subtitleLabel.setControlText(nil) + return + } + + view.titleLabel.setControlText(state.title) + view.titleLabel.apply(truncationStyle: state.titleTruncationStyle) + view.subtitleLabel.setControlText(state.subtitle) + view.subtitleLabel.apply(truncationStyle: state.subtitleTruncationStyle) + } +} diff --git a/FunctionalTableDataDemo/Cells/Subtitle/SubtitleView.swift b/FunctionalTableDataDemo/Cells/Subtitle/SubtitleView.swift new file mode 100644 index 0000000..fb3e067 --- /dev/null +++ b/FunctionalTableDataDemo/Cells/Subtitle/SubtitleView.swift @@ -0,0 +1,47 @@ +// +// SubtitleCell.swift +// Shopify +// +// Created by Raul Riera on 2017-09-21. +// Copyright © 2017 Shopify. All rights reserved. +// + +import UIKit +import FunctionalTableData + +public class SubtitleView: UIView { + let stackView = UIStackView() + let titleLabel = UILabel() + let subtitleLabel = UILabel() + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) is not implemented") + } + + fileprivate func setup() { + stackView.axis = .vertical + stackView.alignment = .leading + stackView.spacing = 4 + + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subtitleLabel) + + stackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor) + ]) + + titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) + subtitleLabel.setContentCompressionResistancePriority(.required, for: .vertical) + } +} diff --git a/FunctionalTableDataDemo/Cells/Switch/SwitchState.swift b/FunctionalTableDataDemo/Cells/Switch/SwitchState.swift new file mode 100644 index 0000000..59069d2 --- /dev/null +++ b/FunctionalTableDataDemo/Cells/Switch/SwitchState.swift @@ -0,0 +1,45 @@ +// +// SwitchState.swift +// Shopify +// +// Created by Raul Riera on 02/02/2017. +// Copyright © 2017 Shopify. All rights reserved. +// + +import UIKit +import FunctionalTableData + +public typealias SwitchCell = HostCell, CombinedState, LayoutMarginsTableItemLayout> + +public struct SwitchState: Equatable { + let isEnabled: Bool + let isOn: Bool + let onValueChanged: (Bool) -> Void + + public init(isEnabled: Bool = true, isOn: Bool, onValueChanged: @escaping (Bool) -> Void) { + self.isEnabled = isEnabled + self.isOn = isOn + self.onValueChanged = onValueChanged + } + + public static func updateView(_ view: UISwitch, state: SwitchState?) { + view.isOn = state?.isOn ?? false + view.isEnabled = state?.isEnabled ?? false + view.setAction(for: .valueChanged) { switchView in + state?.onValueChanged(switchView.isOn) + } + // UISwitch won't layout properly if we don't specify a compression resistance + // otherwise it will be moved away by a sibling view + view.setContentCompressionResistancePriority(.required, for: .horizontal) + + // Allocates the bare minimum horizontal space to the switch in a stackview + view.setContentHuggingPriority(.required, for: .horizontal) + + // Prevents stackViews from shrinkwrapping so eagerly to the switch that it cuts off multiline labels + view.setContentHuggingPriority(.defaultLow, for: .vertical) + } + + public static func ==(lhs: SwitchState, rhs: SwitchState) -> Bool { + return lhs.isEnabled == rhs.isEnabled && lhs.isOn == rhs.isOn + } +} diff --git a/FunctionalTableDataDemo/Cells/UIControl+Extensions.swift b/FunctionalTableDataDemo/Cells/UIControl+Extensions.swift new file mode 100644 index 0000000..3855c1e --- /dev/null +++ b/FunctionalTableDataDemo/Cells/UIControl+Extensions.swift @@ -0,0 +1,83 @@ +// +// UIControl+Extensions.swift +// Shopify +// +// Created by Tom Burns on 2016-01-06. +// Copyright © 2016 Shopify. All rights reserved. +// + +import UIKit + +public protocol UIControlClosureAction {} +extension UIControl: UIControlClosureAction {} + +/// An action object to be associated with the control. +public final class Action: NSObject { + let events: UIControl.Event + let action: (_: T) -> Void + + /// Initializes a new Action instance with corresponding events and action to execute. + /// + /// - Parameters: + /// - events: The control-specific events for which the action method is called. + /// - action: The action that will be executed when the control fires a matching UIControlEvents value. + /// - _: The sender of the event. + public init(events: UIControl.Event, action: @escaping (_: T) -> Void) { + self.events = events + self.action = action + } + + @objc dynamic fileprivate func performAction(sender: Any) { + action(sender as! T) + } +} + +public extension UIControlClosureAction where Self: UIControl { + /// Sets a closure to be executed when the user initiates one of the matching control events. + /// + /// - Parameters: + /// - events: The control-specific events for which the action method is called. + /// - action: The action that will be executed when the control fires a matching UIControlEvents value. + /// - Note: Calling this will remove all existing actions + func setAction(for events: UIControl.Event, action: @escaping (_: Self) -> Void) { + setActions([Action(events: events, action: action)]) + } + + /// Adds a series of actions to be executed when a corresponding control event is triggered. + /// + /// - Parameter actions: The set of different actions to add to the control. + /// - Note: Calling this will remove all existing actions. Passing an empty array will remove all and not add any new ones. + func setActions(_ actions: [Action]) { + if let oldActions = self.actions as? [Action] { + oldActions.forEach { + removeTarget($0, action: #selector(Action.performAction(sender:)), for: $0.events) + } + } + actions.forEach { + addTarget($0, action: #selector(Action.performAction(sender:)), for: $0.events) + } + self.actions = actions + } + + /// Appends an action to series of existing actions + /// + /// - Parameter actions: The action to add + func addAction(_ action: Action) { + addTarget(action, action: #selector(Action.performAction(sender:)), for: action.events) + actions.append(action) + } +} + +private extension UIControl { + private static var controlActionAssociatedHandle: UInt8 = 0 + + var actions: [AnyObject] { + get { + let a = objc_getAssociatedObject(self, &UIControl.controlActionAssociatedHandle) + return a as? [AnyObject] ?? [] + } + set { + objc_setAssociatedObject(self, &UIControl.controlActionAssociatedHandle, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } +} diff --git a/FunctionalTableDataDemo/View Controllers/CollectionExampleController.swift b/FunctionalTableDataDemo/View Controllers/CollectionExampleController.swift index 117cf7e..7d92391 100644 --- a/FunctionalTableDataDemo/View Controllers/CollectionExampleController.swift +++ b/FunctionalTableDataDemo/View Controllers/CollectionExampleController.swift @@ -31,9 +31,9 @@ class CollectionExampleController: UICollectionViewController { } private func render() { - let rows: [CellConfigType] = items.enumerated().map { index, item in + let rows: [CellConfigType] = items.enumerated().map { item in return LabelCell( - key: "id-\(index)", + key: "id-\(item.offset)", style: CellStyle(backgroundColor: .lightGray), actions: CellActions( selectionAction: { _ in @@ -44,7 +44,7 @@ class CollectionExampleController: UICollectionViewController { print("\(item) deselected") return .deselected }), - state: LabelState(text: item), + state: LabelState(text: .plain(item.element)), cellUpdater: LabelState.updateView) } diff --git a/FunctionalTableDataDemo/View Controllers/TableExampleController.swift b/FunctionalTableDataDemo/View Controllers/TableExampleController.swift index 2424401..9a4c379 100644 --- a/FunctionalTableDataDemo/View Controllers/TableExampleController.swift +++ b/FunctionalTableDataDemo/View Controllers/TableExampleController.swift @@ -30,19 +30,19 @@ class TableExampleController: UITableViewController { } private func render() { - let rows: [CellConfigType] = items.enumerated().map { index, item in + let rows: [CellConfigType] = items.enumerated().map { item in return LabelCell( - key: "id-\(index)", + key: "id-\(item.offset)", actions: CellActions( selectionAction: { _ in - print("\(item) selected") + print("\(item.offset) selected") return .selected }, deselectionAction: { _ in - print("\(item) deselected") + print("\(item.offset) deselected") return .deselected }), - state: LabelState(text: item), + state: LabelState(text: .plain(item.element)), cellUpdater: LabelState.updateView) } From a66ba757d516e24b305170c1013efb5a50a3ba10 Mon Sep 17 00:00:00 2001 From: Raul Riera Date: Wed, 3 Apr 2019 12:18:43 -0400 Subject: [PATCH 2/2] Example view controller --- FunctionalTableDataDemo/AppDelegate.swift | 6 ++ .../Cells/Button/ButtonState.swift | 12 ++- .../Cells/ImageState.swift | 6 +- .../Cells/Label/LabelState.swift | 2 +- .../Cells/Spacer/SpacerState.swift | 6 +- .../Cells/Spacer/SpacerView.swift | 2 +- .../Cells/Subtitle/SubtitleView.swift | 2 +- .../Cells/Switch/SwitchState.swift | 2 +- .../logo.imageset/Contents.json | 12 +++ .../Assets.xcassets/logo.imageset/logo.pdf | Bin 0 -> 7602 bytes .../Resources/Base.lproj/Main.storyboard | 44 ++++++++- .../TableExampleController.swift | 92 ++++++++++++++++++ 12 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 FunctionalTableDataDemo/Resources/Assets.xcassets/logo.imageset/Contents.json create mode 100644 FunctionalTableDataDemo/Resources/Assets.xcassets/logo.imageset/logo.pdf diff --git a/FunctionalTableDataDemo/AppDelegate.swift b/FunctionalTableDataDemo/AppDelegate.swift index 297acaa..fdd54d1 100644 --- a/FunctionalTableDataDemo/AppDelegate.swift +++ b/FunctionalTableDataDemo/AppDelegate.swift @@ -7,6 +7,7 @@ // import UIKit +import FunctionalTableData @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -14,6 +15,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + Separator.appearance().backgroundColor = UITableView().separatorColor + UIButton.appearance().setTitleColor(.blue, for: .normal) + UIButton.appearance().setTitleColor(UIColor.blue.withAlphaComponent(0.5), for: .highlighted) + return true } diff --git a/FunctionalTableDataDemo/Cells/Button/ButtonState.swift b/FunctionalTableDataDemo/Cells/Button/ButtonState.swift index 2f125a1..ae2246d 100644 --- a/FunctionalTableDataDemo/Cells/Button/ButtonState.swift +++ b/FunctionalTableDataDemo/Cells/Button/ButtonState.swift @@ -14,22 +14,32 @@ typealias ButtonCell = HostCell Void + public init(title: String, isEnabled: Bool = true, alignment: UIControl.ContentHorizontalAlignment = .center, action: @escaping (UIButton) -> Void) { + self.title = title + self.isEnabled = isEnabled + self.alignment = alignment + self.action = action + } + public static func updateView(_ view: UIButton, state: ButtonState?) { guard let state = state else { view.setTitle(nil, for: .normal) view.isEnabled = true + view.contentHorizontalAlignment = .center view.setActions([]) return } view.setTitle(state.title, for: .normal) view.isEnabled = state.isEnabled + view.contentHorizontalAlignment = state.alignment view.setAction(for: .touchUpInside, action: state.action) } public static func ==(lhs: ButtonState, rhs: ButtonState) -> Bool { - return lhs.title == rhs.title && lhs.isEnabled == rhs.isEnabled + return lhs.title == rhs.title && lhs.isEnabled == rhs.isEnabled && lhs.alignment == rhs.alignment } } diff --git a/FunctionalTableDataDemo/Cells/ImageState.swift b/FunctionalTableDataDemo/Cells/ImageState.swift index 1aec114..0f60627 100644 --- a/FunctionalTableDataDemo/Cells/ImageState.swift +++ b/FunctionalTableDataDemo/Cells/ImageState.swift @@ -10,7 +10,6 @@ import UIKit import FunctionalTableData public typealias ImageCell = HostCell -public typealias ImageWithSubtitleCell = CombinedCell public protocol ImageLoadable { func setImage(from: URL, placeholder: UIImage?) @@ -76,3 +75,8 @@ public struct ImageState: Equatable { return lhs.url == rhs.url && lhs.width == rhs.width && lhs.height == rhs.height } } + +extension UIImageView: ImageLoadable { + public func setImage(from: URL, placeholder: UIImage?) {} + public func cancelLoadingImage() {} +} diff --git a/FunctionalTableDataDemo/Cells/Label/LabelState.swift b/FunctionalTableDataDemo/Cells/Label/LabelState.swift index 14c102e..2cf146e 100644 --- a/FunctionalTableDataDemo/Cells/Label/LabelState.swift +++ b/FunctionalTableDataDemo/Cells/Label/LabelState.swift @@ -28,10 +28,10 @@ public struct LabelState: Equatable { public static func updateView(_ view: UILabel, state: LabelState?) { guard let state = state else { - // reset parts of state that need to be reset view.setControlText(nil) return } + view.font = state.font view.textColor = state.textColor view.apply(truncationStyle: state.truncationStyle) diff --git a/FunctionalTableDataDemo/Cells/Spacer/SpacerState.swift b/FunctionalTableDataDemo/Cells/Spacer/SpacerState.swift index 0c4d3fc..dcb430f 100644 --- a/FunctionalTableDataDemo/Cells/Spacer/SpacerState.swift +++ b/FunctionalTableDataDemo/Cells/Spacer/SpacerState.swift @@ -3,7 +3,7 @@ // Shopify // // Created by Raul Riera on 2017-12-18. -// Copyright © 2017 Raul Riera. All rights reserved. +// Copyright © 2017 Shopify. All rights reserved. // import UIKit @@ -14,13 +14,13 @@ public typealias SpacerCell = HostCell, Edge public struct SpacerState: Equatable { public let height: CGFloat - public init(height: CGFloat = 12) { + public init(height: CGFloat = 12.0) { self.height = height } public static func updateView(_ view: View, state: SpacerState?) { view.backgroundColor = UIColor.clear - view.height = state?.height ?? 12 + view.height = state?.height ?? 12.0 } public static func ==(lhs: SpacerState, rhs: SpacerState) -> Bool { diff --git a/FunctionalTableDataDemo/Cells/Spacer/SpacerView.swift b/FunctionalTableDataDemo/Cells/Spacer/SpacerView.swift index 518e0d7..e46e1e2 100644 --- a/FunctionalTableDataDemo/Cells/Spacer/SpacerView.swift +++ b/FunctionalTableDataDemo/Cells/Spacer/SpacerView.swift @@ -3,7 +3,7 @@ // Shopify // // Created by Raul Riera on 2017-12-18. -// Copyright © 2017 Raul Riera. All rights reserved. +// Copyright © 2017 Shopify. All rights reserved. // import UIKit diff --git a/FunctionalTableDataDemo/Cells/Subtitle/SubtitleView.swift b/FunctionalTableDataDemo/Cells/Subtitle/SubtitleView.swift index fb3e067..de495f8 100644 --- a/FunctionalTableDataDemo/Cells/Subtitle/SubtitleView.swift +++ b/FunctionalTableDataDemo/Cells/Subtitle/SubtitleView.swift @@ -41,7 +41,7 @@ public class SubtitleView: UIView { stackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor) ]) - titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) + titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) subtitleLabel.setContentCompressionResistancePriority(.required, for: .vertical) } } diff --git a/FunctionalTableDataDemo/Cells/Switch/SwitchState.swift b/FunctionalTableDataDemo/Cells/Switch/SwitchState.swift index 59069d2..04e74ed 100644 --- a/FunctionalTableDataDemo/Cells/Switch/SwitchState.swift +++ b/FunctionalTableDataDemo/Cells/Switch/SwitchState.swift @@ -22,7 +22,7 @@ public struct SwitchState: Equatable { self.onValueChanged = onValueChanged } - public static func updateView(_ view: UISwitch, state: SwitchState?) { + public static func updateView(_ view: UISwitch, state: SwitchState?) { view.isOn = state?.isOn ?? false view.isEnabled = state?.isEnabled ?? false view.setAction(for: .valueChanged) { switchView in diff --git a/FunctionalTableDataDemo/Resources/Assets.xcassets/logo.imageset/Contents.json b/FunctionalTableDataDemo/Resources/Assets.xcassets/logo.imageset/Contents.json new file mode 100644 index 0000000..c369d6f --- /dev/null +++ b/FunctionalTableDataDemo/Resources/Assets.xcassets/logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "logo.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FunctionalTableDataDemo/Resources/Assets.xcassets/logo.imageset/logo.pdf b/FunctionalTableDataDemo/Resources/Assets.xcassets/logo.imageset/logo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..88d390be39fb153df6e7e40f712b52544bad4a50 GIT binary patch literal 7602 zcmb_>2UJtr)-_dn5fBs*BA3vlB%uUEdM_Hf^b!aig7hjy0qI?e0@9lx2ukl=dIym% zAWfu5N4~(7_w;ec_{RVJFC!x-S!bVh_Fi+ZnK|YGDoILnfH}GGfh~*Mi)%SMulrlt z@Oc0r01|GAfB!y!OA%pf=4cM!MTJxVT+$ZSjtG0y(b~unA&D?Xnjiqe!uSr3_6Q>z zd{=a@xN2Kk0aB@*pZIOW6>o4l;kNuPUr27U&x(HX-#^1lo!<@BCaa0`S*g=Asr^=X zq+SRTOp}AJUX&3fkzeN+d53SpTe7S|kC-xP|PX*mRX!mQF=L%1{t2ny) znGVMX7kKY{l2xOiV^g_SXzHLk$ZXwl6sU?NEBJDqFe{(QiiS0et~ImDgH{(~NRdwT z9<{`g8;cEDv~;H9)-Kk3=RNVLxU|&3@Q68; d$H}>t=yAG2+`;F`y22gB_Jo?DB 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 - + - + @@ -21,6 +21,7 @@ + @@ -111,6 +112,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FunctionalTableDataDemo/View Controllers/TableExampleController.swift b/FunctionalTableDataDemo/View Controllers/TableExampleController.swift index 9a4c379..d594215 100644 --- a/FunctionalTableDataDemo/View Controllers/TableExampleController.swift +++ b/FunctionalTableDataDemo/View Controllers/TableExampleController.swift @@ -52,3 +52,95 @@ class TableExampleController: UITableViewController { } } +class ExampleViewController: UITableViewController { + private let functionalData = FunctionalTableData() + + override func viewDidLoad() { + super.viewDidLoad() + + functionalData.tableView = tableView + title = "Example View" + + render() + } + + private func render() { + functionalData.renderAndDiff([ + userTableSection(key: "user.section"), + spacerSection(key: "spacer.user"), + networkSection(key: "network.section"), + spacerSection(key: "spacer.network"), + resetSection(key: "reset.section"), + ]) + } + + private func userTableSection(key: String) -> TableSection { + typealias ImageSubtitleCell = HostCell, CombinedState, LayoutMarginsTableItemLayout> + + let title = ControlText.attributed(NSAttributedString(string: "Shopify Inc.", attributes: [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 24, weight: .medium) + ])) + let subtitle = ControlText.attributed(NSAttributedString(string: "Build your business. You’ve got the will. We’ve got the way.", attributes: [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12, weight: .regular), + NSAttributedString.Key.foregroundColor: UIColor.darkGray + ])) + + let imageState = ImageState(image: UIImage(named: "logo")!, width: 58, height: 58) + let subtitleState = SubtitleState(title: title, subtitle: subtitle) + + let rows: [CellConfigType] = [ + ImageSubtitleCell(key: "user.row", style: CellStyle(accessoryType: .disclosureIndicator), state: CombinedState(state1: imageState, state2: subtitleState), cellUpdater: { view, state in + view.stackView.spacing = 16 + + ImageState.updateView(view.view1, state: state?.state1) + SubtitleState.updateView(view.view2, state: state?.state2) + }) + ] + return TableSection(key: key, rows: rows, style: SectionStyle(separators: .topAndBottom)) + } + + private func spacerSection(key: String) -> TableSection { + return TableSection(key: key, rows: [ + SpacerCell(key: "spacer", style: CellStyle(backgroundColor: .clear), state: SpacerState(height: CGFloat(22.0)), cellUpdater: SpacerState.updateView) + ]) + } + + private func networkSection(key: String) -> TableSection { + let airplaneLabelState = LabelState(text: .plain("Airplane mode")) + let airplaneSwitchState = SwitchState(isOn: false) { _ in } + + typealias SubtitleSwitchCell = HostCell, CombinedState, LayoutMarginsTableItemLayout> + + let title = ControlText.attributed(NSAttributedString(string: "Safari App", attributes: [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .regular) + ])) + let subtitle = ControlText.attributed(NSAttributedString(string: "365 MB.", attributes: [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14, weight: .regular), + NSAttributedString.Key.foregroundColor: UIColor.darkGray + ])) + + let exampleAppSubtitleState = SubtitleState(title: title, subtitle: subtitle) + let exampleAppSwitchState = SwitchState(isOn: true) { _ in } + + let rows: [CellConfigType] = [ + SwitchCell(key: "airplane.mode", state: CombinedState(state1: airplaneLabelState, state2: airplaneSwitchState), cellUpdater: { (view, state) in + LabelState.updateView(view.view1, state: state?.state1) + SwitchState.updateView(view.view2, state: state?.state2) + }), + SubtitleSwitchCell(key: "example.app", state: CombinedState(state1: exampleAppSubtitleState, state2: exampleAppSwitchState), cellUpdater: { (view, state) in + SubtitleState.updateView(view.view1, state: state?.state1) + SwitchState.updateView(view.view2, state: state?.state2) + }) + ] + return TableSection(key: key, rows: rows, style: SectionStyle(separators: .default)) + } + + private func resetSection(key: String) -> TableSection { + let rows: [CellConfigType] = [ + ButtonCell(key: "all.settings", state: ButtonState(title: "Reset All Settings", alignment: .left, action: { _ in }), cellUpdater: ButtonState.updateView), + ButtonCell(key: "all.content.settings", state: ButtonState(title: "Reset All Content and Settings", alignment: .left, action: { _ in }), cellUpdater: ButtonState.updateView), + ] + return TableSection(key: key, rows: rows, style: SectionStyle(separators: .default)) + } +} +