├── Examples
├── TodoMVC
│ ├── Source
│ │ ├── Common.css
│ │ ├── Header.css
│ │ ├── Header.xml
│ │ ├── Todo.xml
│ │ ├── Assets.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── CountText.swift
│ │ ├── Footer.xml
│ │ ├── ViewController.swift
│ │ ├── Info.plist
│ │ ├── Base.lproj
│ │ │ ├── Main.storyboard
│ │ │ └── LaunchScreen.storyboard
│ │ ├── AppDelegate.swift
│ │ ├── Todos.swift
│ │ ├── Footer.swift
│ │ └── Todo.swift
│ └── TodoMVC.xcodeproj
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── TodoMVC.xcscheme
├── Twitter
│ ├── Cartfile
│ ├── Cartfile.resolved
│ ├── Source
│ │ ├── Tweet.xml
│ │ ├── Assets.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── ViewController.swift
│ │ ├── TweetItem.swift
│ │ ├── Info.plist
│ │ ├── Base.lproj
│ │ │ ├── Main.storyboard
│ │ │ └── LaunchScreen.storyboard
│ │ ├── AppDelegate.swift
│ │ ├── App.swift
│ │ └── OrderedDictionary.swift
│ └── Twitter.xcodeproj
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── Twitter.xcscheme
└── SimpleTable
│ └── Source
│ ├── Item.xml
│ ├── Header.xml
│ ├── ItemModel.swift
│ ├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Item.swift
│ ├── ViewController.swift
│ ├── Info.plist
│ ├── Base.lproj
│ ├── Main.storyboard
│ └── LaunchScreen.storyboard
│ ├── AppDelegate.swift
│ └── App.swift
├── Tests
├── Resources
│ ├── TemplateWithModel.xml
│ └── SimpleTemplate.xml
├── Info.plist
├── XMLTemplateServiceTests.swift
├── TemplateTests.swift
├── DiffTests.swift
└── NodeTests.swift
├── Cartfile
├── TemplateKit.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ ├── TemplateKitTests.xcscheme
│ └── TemplateKit.xcscheme
├── Source
├── Core
│ ├── Keyable.swift
│ ├── Animation
│ │ ├── Interpolator.swift
│ │ ├── Component+Animation.swift
│ │ ├── Interpolatable.swift
│ │ ├── Animatable.swift
│ │ ├── Animator.swift
│ │ ├── Bezier.swift
│ │ └── BezierInterpolator.swift
│ ├── Properties
│ │ ├── RawProperties.swift
│ │ ├── DefaultProperties.swift
│ │ ├── GestureProperties.swift
│ │ ├── Properties.swift
│ │ ├── IdentifierProperties.swift
│ │ ├── CoreProperties.swift
│ │ ├── TextStyleProperties.swift
│ │ └── StyleProperties.swift
│ ├── View.swift
│ ├── NativeView.swift
│ ├── ViewNode.swift
│ ├── Renderer.swift
│ ├── NativeNode.swift
│ ├── Rendering.swift
│ ├── StyleSheet+Element.swift
│ ├── Element.swift
│ └── Node.swift
├── Template
│ ├── TemplateService.swift
│ ├── Error.swift
│ ├── Template.swift
│ ├── NodeRegistry.swift
│ ├── Model.swift
│ └── XMLTemplateService.swift
├── Utilities
│ ├── String.swift
│ ├── Result.swift
│ ├── URL.swift
│ ├── Dictionary.swift
│ ├── Array.swift
│ ├── AsyncQueue.swift
│ ├── XMLDocument.swift
│ ├── ResourceService.swift
│ └── Diff.swift
├── iOS
│ ├── ScrollProxy.swift
│ ├── ImageService.swift
│ ├── Box.swift
│ ├── DelegateProxy.swift
│ ├── ActivityIndicator.swift
│ ├── Image.swift
│ ├── UIView.swift
│ ├── UIKitRenderer.swift
│ ├── Button.swift
│ ├── Text.swift
│ ├── TextField.swift
│ ├── AsyncDataListView.swift
│ └── Table.swift
├── TemplateKit.h
└── Info.plist
├── Cartfile.resolved
├── .gitignore
├── TemplateKit.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── TemplateKit.xcscmblueprint
├── TemplateKit.podspec
└── LICENSE.md
/Examples/TodoMVC/Source/Common.css:
--------------------------------------------------------------------------------
1 | textfield {
2 | borderColor: #eeeeee;
3 | }
4 |
--------------------------------------------------------------------------------
/Tests/Resources/TemplateWithModel.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Cartfile:
--------------------------------------------------------------------------------
1 | github "mcudich/CSSParser.git" "master"
2 | github "mcudich/CSSLayout.git" "master"
3 |
--------------------------------------------------------------------------------
/Examples/Twitter/Cartfile:
--------------------------------------------------------------------------------
1 | github "krzyzanowskim/CryptoSwift"
2 | github "SwiftyJSON/SwiftyJSON"
3 | github "malcommac/SwiftDate"
4 | github "Alamofire/Alamofire"
5 |
--------------------------------------------------------------------------------
/Examples/Twitter/Cartfile.resolved:
--------------------------------------------------------------------------------
1 | github "Alamofire/Alamofire" "4.0.1"
2 | github "krzyzanowskim/CryptoSwift" "0.6.4"
3 | github "malcommac/SwiftDate" "4.0.7"
4 | github "SwiftyJSON/SwiftyJSON" "v3.1.1"
5 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/Item.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/TemplateKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/Resources/SimpleTemplate.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Source/Core/Keyable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Keyable.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/9/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Keyable {
12 | var key: String? { get set }
13 | }
14 |
--------------------------------------------------------------------------------
/Cartfile.resolved:
--------------------------------------------------------------------------------
1 | github "facebook/css-layout" "620cb3f507a06b71ba080dfb7aa453e4ada574c9"
2 | github "mcudich/katana-parser" "aa656934a73f8c869789f8d1c852b98322df13eb"
3 | github "mcudich/CSSLayout" "4dec53c006f0d5c53b5ae5cfd1773a081d4ed1b4"
4 | github "mcudich/CSSParser" "00ff2ee18ff056d2c7626d303eb51444446ef7da"
5 |
--------------------------------------------------------------------------------
/Source/Template/TemplateService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol TemplateService {
4 | var templates: [URL: Template] { get }
5 | func fetchTemplates(withURLs urls: [URL], completion: @escaping (Result) -> Void)
6 | func addObserver(observer: Node, forLocation location: URL)
7 | func removeObserver(observer: Node, forLocation location: URL)
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | build/
4 | *.pbxuser
5 | !default.pbxuser
6 | *.mode1v3
7 | !default.mode1v3
8 | *.mode2v3
9 | !default.mode2v3
10 | *.perspectivev3
11 | !default.perspectivev3
12 | xcuserdata
13 | *.xccheckout
14 | *.moved-aside
15 | DerivedData
16 | *.hmap
17 | *.ipa
18 | *.xcuserstate
19 | **/Carthage/Checkouts
20 | **/Carthage/Build
21 |
22 | # OSX
23 | .DS_Store
24 |
--------------------------------------------------------------------------------
/Source/Core/Animation/Interpolator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interpolator.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/22/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Interpolator {
12 | func interpolate(_ fromValue: T, _ toValue: T, _ elapsed: TimeInterval, _ duration: TimeInterval) -> T
13 | }
14 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/Header.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Source/Template/Error.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Error.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 8/23/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum TemplateKitError: Error {
12 | case parserError(String)
13 | case missingTemplate(String)
14 | case missingProvider(String)
15 | case missingPropertyTypes(String)
16 | case missingNodeType(String)
17 | }
18 |
--------------------------------------------------------------------------------
/Source/Utilities/String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/26/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension String {
12 | func capitalizingFirstLetter() -> String {
13 | let first = String(characters.prefix(1)).capitalized
14 | let other = String(characters.dropFirst())
15 | return first + other
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/ItemModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItemModel.swift
3 | // SimpleTable
4 | //
5 | // Created by Matias Cudich on 11/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct ItemModel: Hashable {
12 | let value: String
13 |
14 | var hashValue: Int {
15 | return value.hashValue
16 | }
17 | }
18 |
19 | func ==(lhs: ItemModel, rhs: ItemModel) -> Bool {
20 | return lhs.value == rhs.value
21 | }
22 |
--------------------------------------------------------------------------------
/Source/iOS/ScrollProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScrollProxy.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 11/11/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol ScrollProxyDelegate: class {
12 | func scrollViewDidScroll(_ scrollView: UIScrollView)
13 | }
14 |
15 | public class ScrollProxy: NSObject {
16 | weak var delegate: ScrollProxyDelegate?
17 |
18 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
19 | delegate?.scrollViewDidScroll(scrollView)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Header.css:
--------------------------------------------------------------------------------
1 | #container {
2 | paddingTop: 40;
3 | fontSize: 20;
4 | }
5 |
6 | #todos {
7 | textAlignment: center;
8 | }
9 |
10 | #fieldContainer {
11 | flexDirection: row;
12 | alignItems: center;
13 | }
14 |
15 | #button {
16 | backgroundColor: #eeeeee;
17 | width: 24;
18 | height: 24;
19 | }
20 |
21 | #button[selected=true] {
22 | backgroundColor: #00ff00;
23 | }
24 |
25 | textfield {
26 | height: 24;
27 | flexGrow: 1;
28 | }
29 |
30 | #editButton {
31 | fontSize: 14;
32 | marginLeft: 10;
33 | marginRight: 10;
34 | }
35 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Header.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Source/TemplateKit.h:
--------------------------------------------------------------------------------
1 | //
2 | // TemplateKit.h
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 8/7/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for TemplateKit.
12 | FOUNDATION_EXPORT double TemplateKitVersionNumber;
13 |
14 | //! Project version string for TemplateKit.
15 | FOUNDATION_EXPORT const unsigned char TemplateKitVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Examples/Twitter/Source/Tweet.xml:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Source/Utilities/Result.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Result.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 8/22/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum Result {
12 | case success(ResultType)
13 | case failure(Error)
14 |
15 | var payload: ResultType? {
16 | if case let .success(result) = self {
17 | return result
18 | }
19 | return nil
20 | }
21 |
22 | var error: Error? {
23 | if case let .failure(error) = self {
24 | return error
25 | }
26 | return nil
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Source/Core/Properties/RawProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RawProperties.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/5/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol RawProperties {
12 | init(_ properties: [String: Any])
13 | mutating func merge(_ other: Self)
14 |
15 | mutating func merge(_ value: inout T?, _ newValue: T?)
16 | }
17 |
18 | public extension RawProperties {
19 | public mutating func merge(_ value: inout T?, _ newValue: T?) {
20 | if let newValue = newValue {
21 | value = newValue
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Source/Core/View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/18/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CSSLayout
11 |
12 | public protocol View: class {
13 | var frame: CGRect { get set }
14 | var parent: View? { get }
15 | var children: [View] { get set }
16 |
17 | func add(_ view: View)
18 | func replace(_ view: View, with newView: View)
19 | }
20 |
21 | extension CSSLayout {
22 | func apply(to view: View) {
23 | view.frame = frame
24 |
25 | for (index, child) in children.enumerated() {
26 | child.apply(to: view.children[index])
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/TemplateKit.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
10 |
12 |
13 |
15 |
16 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Source/Core/NativeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NativeView.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/5/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct EventRecognizers {
12 | typealias Recognizer = (Selector, UIGestureRecognizer)
13 | var onTap: Recognizer?
14 | var onPress: Recognizer?
15 | var onDoubleTap: Recognizer?
16 |
17 | public init() {}
18 | }
19 |
20 | public protocol NativeView: View {
21 | associatedtype PropertiesType: Properties
22 |
23 | var properties: PropertiesType { get set }
24 | var eventRecognizers: EventRecognizers { get set }
25 | weak var eventTarget: AnyObject? { get set }
26 |
27 | init()
28 |
29 | func touchesBegan()
30 | }
31 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Todo.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Source/iOS/ImageService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageService.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 8/11/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class ImageParser: Parser {
12 | typealias ParsedType = UIImage
13 |
14 | required init() {}
15 |
16 | func parse(data: Data) throws -> UIImage {
17 | guard let image = UIImage(data: data) else {
18 | throw TemplateKitError.parserError("Invalid image data")
19 | }
20 | return image
21 | }
22 | }
23 |
24 | class ImageService: ResourceService {
25 | static let shared = ImageService()
26 |
27 | convenience init() {
28 | self.init(requestQueue: DispatchQueue(label: "ImageService"))
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/TemplateKit.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "TemplateKit"
3 | s.version = "0.1.0"
4 | s.summary = "Native UI components in Swift."
5 | s.description = "React-inspired framework for building component-based user interfaces in Swift."
6 |
7 | s.homepage = "https://github.com/mcudich/TemplateKit"
8 | s.license = "MIT"
9 | s.author = { "Matias Cudich" => "mcudich@gmail.com" }
10 | s.source = { :git => "https://github.com/mcudich/TemplateKit.git", :tag => s.version.to_s }
11 | s.social_media_url = "https://twitter.com/mcudich"
12 |
13 | s.ios.deployment_target = "9.3"
14 |
15 | s.source_files = "Source/**/*"
16 |
17 | s.dependency "CSSParser", "~> 1.0"
18 | s.dependency "CSSLayout", "~> 1.0"
19 | end
20 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/Examples/Twitter/Source/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/Source/Utilities/URL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/30/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension URL: ExpressibleByStringLiteral {
12 | public typealias StringLiteralType = String
13 | public typealias UnicodeScalarLiteralType = String
14 | public typealias ExtendedGraphemeClusterLiteralType = String
15 |
16 | public init(stringLiteral value: URL.StringLiteralType) {
17 | self = URL(string: value)!
18 | }
19 | public init(extendedGraphemeClusterLiteral value: URL.ExtendedGraphemeClusterLiteralType) {
20 | self = URL(string: value)!
21 | }
22 | public init(unicodeScalarLiteral value: URL.UnicodeScalarLiteralType) {
23 | self = URL(string: value)!
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Source/Utilities/Dictionary.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dictionary.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 8/10/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Dictionary {
12 | mutating func merge(with dictionary: Dictionary) {
13 | for (key, value) in dictionary {
14 | self[key] = value
15 | }
16 | }
17 |
18 | func merged(with dictionary: Dictionary) -> Dictionary {
19 | var copy = self
20 | copy.merge(with: dictionary)
21 | return copy
22 | }
23 | }
24 |
25 | func ==(lhs: [K: V]?, rhs: [K: V]?) -> Bool {
26 | switch (lhs, rhs) {
27 | case let (l?, r?):
28 | return l == r
29 | case (.none, .none):
30 | return true
31 | default:
32 | return false
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Source/Core/Properties/DefaultProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultProperties.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/5/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct DefaultProperties: Properties {
12 | public var core = CoreProperties()
13 | public var textStyle = TextStyleProperties()
14 |
15 | public init() {}
16 |
17 | public init(_ properties: [String: Any]) {
18 | core = CoreProperties(properties)
19 | textStyle = TextStyleProperties(properties)
20 | }
21 |
22 | public mutating func merge(_ other: DefaultProperties) {
23 | core.merge(other.core)
24 | textStyle.merge(other.textStyle)
25 | }
26 | }
27 |
28 | public func ==(lhs: DefaultProperties, rhs: DefaultProperties) -> Bool {
29 | return lhs.equals(otherProperties: rhs)
30 | }
31 |
--------------------------------------------------------------------------------
/Source/iOS/Box.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Box.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class Box: UIView, NativeView {
12 | public weak var eventTarget: AnyObject?
13 | public lazy var eventRecognizers = EventRecognizers()
14 |
15 | public var properties = DefaultProperties() {
16 | didSet {
17 | applyCoreProperties()
18 | }
19 | }
20 |
21 | public required init() {
22 | super.init(frame: CGRect.zero)
23 | }
24 |
25 | required public init?(coder aDecoder: NSCoder) {
26 | fatalError("init(coder:) has not been implemented")
27 | }
28 |
29 | public override func touchesBegan(_ touches: Set, with event: UIEvent?) {
30 | super.touchesBegan(touches, with: event)
31 |
32 | touchesBegan()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Source/Template/Template.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CSSParser
3 |
4 | public protocol ElementProvider {
5 | func build(with model: Model) -> Element
6 | func equals(_ other: ElementProvider?) -> Bool
7 | }
8 |
9 | public struct Template: Equatable {
10 | fileprivate let elementProvider: ElementProvider
11 | fileprivate let styleSheet: StyleSheet?
12 |
13 | public init(_ elementProvider: ElementProvider, _ styleSheet: StyleSheet? = nil) {
14 | self.elementProvider = elementProvider
15 | self.styleSheet = styleSheet
16 | }
17 |
18 | public func build(with model: Model) -> Element {
19 | let tree = elementProvider.build(with: model)
20 | styleSheet?.apply(to: tree)
21 | return tree
22 | }
23 | }
24 |
25 | public func ==(lhs: Template, rhs: Template) -> Bool {
26 | return lhs.elementProvider.equals(rhs.elementProvider) && lhs.styleSheet == rhs.styleSheet
27 | }
28 |
--------------------------------------------------------------------------------
/Source/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Source/Core/ViewNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OpaqueView.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/8/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CSSLayout
11 |
12 | class ViewNode: PropertyNode {
13 | weak var owner: Node?
14 | weak var parent: Node?
15 | var context: Context?
16 |
17 | var properties: DefaultProperties
18 | var children: [Node]?
19 | var element: ElementData
20 | var cssNode: CSSNode?
21 |
22 | let view: View
23 |
24 | init(view: ViewType, element: ElementData, owner: Node? = nil, context: Context? = nil) {
25 | self.view = view
26 | self.element = element
27 | self.properties = self.element.properties
28 | self.owner = owner
29 | self.context = context
30 | }
31 |
32 | func build() -> View {
33 | return view
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/XMLTemplateServiceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TemplateTests.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/27/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import TemplateKit
11 | import CSSParser
12 |
13 | class XMLTemplateServiceTests: XCTestCase {
14 | struct FakeModel: Model {}
15 |
16 | func testParseTemplate() {
17 | let template = Bundle(for: TemplateTests.self).url(forResource: "SimpleTemplate", withExtension: "xml")!
18 | let xmlTemplate = try! XMLDocument(data: Data(contentsOf: template))
19 | let styleSheet = StyleSheet(string: xmlTemplate.styleElements.first!.value!)!
20 | let parsed = Template(xmlTemplate.componentElement!, styleSheet)
21 | let element = parsed.build(with: FakeModel()) as! ElementData
22 | XCTAssertEqual(UIColor.red, element.properties.core.style.backgroundColor)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Examples/Twitter/Source/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // TwitterClientExample
4 | //
5 | // Created by Matias Cudich on 10/27/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | import TemplateKit
12 |
13 | class ViewController: UIViewController {
14 | var app: Node?
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 |
19 | var properties = DefaultProperties()
20 | properties.core.layout.width = Float(view.bounds.size.width)
21 | properties.core.layout.height = Float(view.bounds.size.height)
22 |
23 | let templates: [URL] = [TweetItem.templateURL]
24 | UIKitRenderer.defaultContext.templateService.fetchTemplates(withURLs: templates) { result in
25 | UIKitRenderer.render(component(App.self, properties), container: self.view, context: nil) { component in
26 | self.app = component
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Source/Core/Properties/GestureProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GestureProperties.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/5/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct GestureProperties: RawProperties, Model, Equatable {
12 | var onTap: Selector?
13 | var onPress: Selector?
14 | var onDoubleTap: Selector?
15 |
16 | public init() {}
17 |
18 | public init(_ properties: [String : Any]) {
19 | onTap = properties.cast("onTap")
20 | onPress = properties.cast("onPress")
21 | onDoubleTap = properties.cast("onDoubleTap")
22 | }
23 |
24 | public mutating func merge(_ other: GestureProperties) {
25 | merge(&onTap, other.onTap)
26 | merge(&onPress, other.onPress)
27 | merge(&onDoubleTap, other.onDoubleTap)
28 | }
29 | }
30 |
31 | public func ==(lhs: GestureProperties, rhs: GestureProperties) -> Bool {
32 | return lhs.onTap == rhs.onTap
33 | }
34 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/Item.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Item.swift
3 | // SimpleTable
4 | //
5 | // Created by Matias Cudich on 11/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | import TemplateKit
12 |
13 | struct ItemProperties: Properties {
14 | var core = CoreProperties()
15 | var item: String?
16 |
17 | init() {}
18 |
19 | init(_ properties: [String : Any]) {}
20 |
21 | mutating func merge(_ other: ItemProperties) {
22 | core.merge(other.core)
23 | merge(&item, other.item)
24 | }
25 | }
26 |
27 | func ==(lhs: ItemProperties, rhs: ItemProperties) -> Bool {
28 | return lhs.item == rhs.item && lhs.equals(otherProperties: rhs)
29 | }
30 |
31 | class Item: Component {
32 | static let templateURL = Bundle.main.url(forResource: "Item", withExtension: "xml")!
33 |
34 | override func render() -> Template {
35 | return render(Item.templateURL)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // SimpleTable
4 | //
5 | // Created by Matias Cudich on 11/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | import TemplateKit
12 |
13 | class ViewController: UIViewController {
14 | var app: Node?
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 |
19 | var properties = DefaultProperties()
20 | properties.core.layout.width = Float(view.bounds.size.width)
21 | properties.core.layout.height = Float(view.bounds.size.height)
22 |
23 | let templates: [URL] = [Item.templateURL, Bundle.main.url(forResource: "Header", withExtension: "xml")!]
24 | UIKitRenderer.defaultContext.templateService.fetchTemplates(withURLs: templates) { result in
25 | UIKitRenderer.render(component(App.self, properties), container: self.view, context: nil) { component in
26 | self.app = component
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Examples/Twitter/Source/TweetItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tweet.swift
3 | // TwitterClientExample
4 | //
5 | // Created by Matias Cudich on 10/27/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | import TemplateKit
12 |
13 | struct TweetProperties: Properties {
14 | var core = CoreProperties()
15 | var tweet: Tweet?
16 |
17 | init() {}
18 |
19 | init(_ properties: [String : Any]) {}
20 |
21 | mutating func merge(_ other: TweetProperties) {
22 | core.merge(other.core)
23 | merge(&tweet, other.tweet)
24 | }
25 | }
26 |
27 | func ==(lhs: TweetProperties, rhs: TweetProperties) -> Bool {
28 | return lhs.tweet == rhs.tweet && lhs.equals(otherProperties: rhs)
29 | }
30 |
31 | class TweetItem: Component {
32 | static let templateURL = Bundle.main.url(forResource: "Tweet", withExtension: "xml")!
33 |
34 | override func render() -> Template {
35 | return render(TweetItem.templateURL)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Source/Core/Properties/Properties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Properties.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/19/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Properties: RawProperties, Equatable {
12 | var core: CoreProperties { get set }
13 |
14 | init()
15 | func equals(otherProperties: T) -> Bool
16 | func has(key: String, withValue value: String) -> Bool
17 | }
18 |
19 | public extension Properties {
20 | public func equals(otherProperties: T) -> Bool {
21 | return core == otherProperties.core
22 | }
23 |
24 | public func has(key: String, withValue value: String) -> Bool {
25 | return false
26 | }
27 | }
28 |
29 | public protocol FocusableProperties {
30 | var focused: Bool? { get set }
31 | }
32 |
33 | public protocol EnableableProperties {
34 | var enabled: Bool? { get set }
35 | }
36 |
37 | public protocol ActivatableProperties {
38 | var active: Bool? { get set }
39 | }
40 |
--------------------------------------------------------------------------------
/Source/Core/Properties/IdentifierProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IdentifierProperties.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/5/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct IdentifierProperties: RawProperties, Model, Equatable {
12 | var key: String?
13 | var id: String?
14 | var classNames: [String]?
15 |
16 | public init() {}
17 |
18 | public init(_ properties: [String : Any]) {
19 | key = properties.cast("key")
20 | id = properties.cast("id")
21 | if let classNames: String = properties.cast("classNames") {
22 | self.classNames = classNames.components(separatedBy: " ")
23 | }
24 | }
25 |
26 | public mutating func merge(_ other: IdentifierProperties) {
27 | merge(&key, other.key)
28 | merge(&id, other.id)
29 | merge(&classNames, other.classNames)
30 | }
31 | }
32 |
33 | public func ==(lhs: IdentifierProperties, rhs: IdentifierProperties) -> Bool {
34 | return lhs.key == rhs.key && lhs.id == rhs.id && lhs.classNames == rhs.classNames
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Matias Cudich
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Source/Core/Animation/Component+Animation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Component+Animation.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/23/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Component: AnimatorObserver {
12 | public var hashValue: Int {
13 | return ObjectIdentifier(self as AnyObject).hashValue
14 | }
15 |
16 | public func didAnimate() {
17 | let root = self.root
18 | getContext().updateQueue.async {
19 | self.update(with: self.element, force: true)
20 | let layout = root.computeLayout()
21 | DispatchQueue.main.async {
22 | _ = self.build()
23 | layout.apply(to: root.view)
24 | }
25 | }
26 | }
27 |
28 | public func animate(_ animatable: Animatable, to value: T) {
29 | animatable.set(value)
30 | Animator.shared.addAnimatable(animatable)
31 | Animator.shared.addObserver(self, for: animatable)
32 | }
33 |
34 | public func equals(_ other: AnimatorObserver) -> Bool {
35 | guard let other = other as? Component else {
36 | return false
37 | }
38 | return self == other
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Source/Core/Properties/CoreProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CoreProperties.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/5/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct CoreProperties: RawProperties, Equatable {
12 | public var identifier = IdentifierProperties()
13 | public var layout = LayoutProperties()
14 | public var style = StyleProperties()
15 | public var gestures = GestureProperties()
16 |
17 | public init() {}
18 |
19 | public init(_ properties: [String : Any]) {
20 | identifier = IdentifierProperties(properties)
21 | layout = LayoutProperties(properties)
22 | style = StyleProperties(properties)
23 | gestures = GestureProperties(properties)
24 | }
25 |
26 | public mutating func merge(_ other: CoreProperties) {
27 | identifier.merge(other.identifier)
28 | layout.merge(other.layout)
29 | style.merge(other.style)
30 | gestures.merge(other.gestures)
31 | }
32 | }
33 |
34 | public func ==(lhs: CoreProperties, rhs: CoreProperties) -> Bool {
35 | return lhs.identifier == rhs.identifier && lhs.layout == rhs.layout && lhs.style == rhs.style && lhs.gestures == rhs.gestures
36 | }
37 |
--------------------------------------------------------------------------------
/Source/Core/Renderer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Renderer.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/8/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Context {
12 | var templateService: TemplateService { get }
13 | var updateQueue: DispatchQueue { get }
14 | }
15 |
16 | public protocol Renderer {
17 | associatedtype ViewType: View
18 | static func render(_ element: Element, container: ViewType?, context: Context?, completion: @escaping (Node) -> Void)
19 | static var defaultContext: Context { get }
20 | }
21 |
22 | public extension Renderer {
23 | static func render(_ element: Element, container: ViewType? = nil, context: Context? = nil, completion: @escaping (Node) -> Void) {
24 | let context = context ?? defaultContext
25 | context.updateQueue.async {
26 | let component = element.build(withOwner: nil, context: context)
27 | let layout = component.computeLayout()
28 |
29 | DispatchQueue.main.async {
30 | let builtView = component.build()
31 | layout.apply(to: builtView)
32 | container?.add(builtView)
33 | completion(component)
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Examples/Twitter/Source/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Source/Core/NativeNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NativeNode.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/7/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CSSLayout
11 |
12 | class NativeNode: PropertyNode {
13 | weak var parent: Node?
14 | weak var owner: Node?
15 | var context: Context?
16 |
17 | var properties: T.PropertiesType
18 | var children: [Node]? {
19 | didSet {
20 | updateParent()
21 | }
22 | }
23 | var element: ElementData
24 | var cssNode: CSSNode?
25 |
26 | lazy var view: View = T()
27 |
28 | required init(element: ElementData, children: [Node]? = nil, owner: Node? = nil, context: Context? = nil) {
29 | self.element = element
30 | self.properties = element.properties
31 | self.children = children
32 | self.owner = owner
33 | self.context = context
34 |
35 | updateParent()
36 | }
37 |
38 | func build() -> View {
39 | let view = self.view as! T
40 |
41 | view.eventTarget = owner
42 | if view.properties != properties {
43 | view.properties = properties
44 | }
45 |
46 | if let children = children {
47 | view.children = children.map { $0.build() }
48 | }
49 |
50 | return view
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/CountText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CountText.swift
3 | // Example
4 | //
5 | // Created by Matias Cudich on 10/8/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import TemplateKit
11 |
12 | struct CountTextProperties: Properties {
13 | static var tagName = "CountText"
14 |
15 | var core = CoreProperties()
16 | var textStyle = TextStyleProperties()
17 |
18 | var count: String?
19 |
20 | init() {}
21 |
22 | init(_ properties: [String: Any]) {
23 | core = CoreProperties(properties)
24 | textStyle = TextStyleProperties(properties)
25 |
26 | count = properties.cast("count")
27 | }
28 |
29 | mutating func merge(_ other: CountTextProperties) {
30 | core.merge(other.core)
31 | textStyle.merge(other.textStyle)
32 |
33 | merge(&count, other.count)
34 | }
35 | }
36 |
37 | func ==(lhs: CountTextProperties, rhs: CountTextProperties) -> Bool {
38 | return lhs.count == rhs.count && lhs.equals(otherProperties: rhs)
39 | }
40 |
41 | class CountText: Component {
42 | override func render() -> Template {
43 | var properties = TextProperties()
44 | properties.text = self.properties.count
45 | properties.textStyle = self.properties.textStyle
46 |
47 | return Template(text(properties))
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Footer.xml:
--------------------------------------------------------------------------------
1 |
2 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Source/Core/Properties/TextStyleProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextStyleProperties.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/5/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct TextStyleProperties: RawProperties, Equatable {
12 | public var fontName: String?
13 | public var fontSize: CGFloat?
14 | public var color: UIColor?
15 | public var lineBreakMode: NSLineBreakMode?
16 | public var textAlignment: NSTextAlignment?
17 |
18 | public init() {}
19 |
20 | public init(_ properties: [String : Any]) {
21 | fontName = properties.cast("fontName")
22 | fontSize = properties.cast("fontSize")
23 | color = properties.color("color")
24 | lineBreakMode = properties.cast("lineBreakMode")
25 | textAlignment = properties.cast("textAlignment")
26 | }
27 |
28 | public mutating func merge(_ other: TextStyleProperties) {
29 | merge(&fontName, other.fontName)
30 | merge(&fontSize, other.fontSize)
31 | merge(&color, other.color)
32 | merge(&lineBreakMode, other.lineBreakMode)
33 | merge(&textAlignment, other.textAlignment)
34 | }
35 | }
36 |
37 | public func ==(lhs: TextStyleProperties, rhs: TextStyleProperties) -> Bool {
38 | return lhs.fontName == rhs.fontName && lhs.fontSize == rhs.fontSize && lhs.color == rhs.color && lhs.lineBreakMode == rhs.lineBreakMode && lhs.textAlignment == rhs.textAlignment
39 | }
40 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Example
4 | //
5 | // Created by Matias Cudich on 8/7/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import TemplateKit
11 |
12 | class ViewController: UIViewController, Context {
13 | lazy var templateService: TemplateService = {
14 | let templateService = XMLTemplateService(liveReload: false)
15 | templateService.cachePolicy = .never
16 | return templateService
17 | }()
18 |
19 | var updateQueue: DispatchQueue {
20 | return UIKitRenderer.defaultContext.updateQueue
21 | }
22 |
23 | private var app: App?
24 |
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 |
28 | var properties = AppProperties()
29 | properties.core.layout.width = Float(view.bounds.size.width)
30 | properties.core.layout.height = Float(view.bounds.size.height)
31 | properties.model = Todos()
32 |
33 | let templateURLs = [
34 | App.headerTemplateURL,
35 | Footer.templateURL,
36 | Todo.templateURL
37 | ]
38 |
39 | NodeRegistry.shared.registerComponent(CountText.self, CountTextProperties.self)
40 |
41 | templateService.fetchTemplates(withURLs: templateURLs) { result in
42 | UIKitRenderer.render(component(App.self, properties), container: self.view, context: self) { component in
43 | self.app = component as? App
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Source/Core/Properties/StyleProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StyleProperties.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/5/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct StyleProperties: RawProperties, Model, Equatable {
12 | public var backgroundColor: UIColor?
13 | public var borderColor: UIColor?
14 | public var borderWidth: CGFloat?
15 | public var cornerRadius: CGFloat?
16 | public var opacity: CGFloat?
17 |
18 | public init() {}
19 |
20 | public init(_ properties: [String : Any]) {
21 | backgroundColor = properties.color("backgroundColor")
22 | borderColor = properties.color("borderColor")
23 | borderWidth = properties.cast("borderWidth")
24 | cornerRadius = properties.cast("cornerRadius")
25 | opacity = properties.cast("opacity")
26 | }
27 |
28 | public mutating func merge(_ other: StyleProperties) {
29 | merge(&backgroundColor, other.backgroundColor)
30 | merge(&borderColor, other.borderColor)
31 | merge(&borderWidth, other.borderWidth)
32 | merge(&cornerRadius, other.cornerRadius)
33 | merge(&opacity, other.opacity)
34 | }
35 | }
36 |
37 | public func ==(lhs: StyleProperties, rhs: StyleProperties) -> Bool {
38 | return lhs.backgroundColor == rhs.backgroundColor && lhs.borderColor == rhs.borderColor && lhs.borderWidth == rhs.borderWidth && lhs.cornerRadius == rhs.cornerRadius && lhs.opacity == rhs.opacity
39 | }
40 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | NSAppTransportSecurity
26 |
27 | NSAllowsArbitraryLoads
28 |
29 |
30 | UILaunchStoryboardName
31 | LaunchScreen
32 | UIMainStoryboardFile
33 | Main
34 | UIRequiredDeviceCapabilities
35 |
36 | armv7
37 |
38 | UISupportedInterfaceOrientations
39 |
40 | UIInterfaceOrientationPortrait
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Tests/TemplateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TemplateTests.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/26/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import TemplateKit
11 | import CSSParser
12 |
13 | class TemplateTests: XCTestCase {
14 | struct FakeModel: Model {}
15 |
16 | func testBuildTemplate() {
17 | let element = box(DefaultProperties(), [])
18 | let template = Template(element)
19 | let built = template.build(with: FakeModel())
20 | XCTAssert(element.equals(built))
21 | }
22 |
23 | func testApplyStylesheet() {
24 | let grandchild = text(TextProperties())
25 | let child = box(DefaultProperties(["classNames": "child"]), [grandchild])
26 | let parent = box(DefaultProperties(["id": "parent"]), [child])
27 |
28 | let stylesheet = StyleSheet(string: "#parent { fontSize: 20 } .child { width: 50 }", inheritedProperties: ["fontSize"])!
29 | let template = Template(parent, stylesheet)
30 | let built = template.build(with: FakeModel()) as! ElementData
31 | let builtChild = built.children?.first as! ElementData
32 | let builtGrandchild = child.children?.first as! ElementData
33 |
34 | XCTAssertEqual(20, built.properties.textStyle.fontSize)
35 | XCTAssertEqual(20, builtChild.properties.textStyle.fontSize)
36 | XCTAssertEqual(20, builtGrandchild.properties.textStyle.fontSize)
37 |
38 | XCTAssertEqual(50, builtChild.properties.core.layout.width)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Source/Utilities/Array.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/8/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Array {
12 | func keyed(by: (Int, Element) -> AnyHashable) -> Dictionary {
13 | var result = [AnyHashable: Element]()
14 | for (index, element) in self.enumerated() {
15 | let key = by(index, element)
16 | if result[key] != nil {
17 | fatalError("Attempting to key by elements that have shared key: \(key)")
18 | }
19 | result[key] = element
20 | }
21 | return result
22 | }
23 | }
24 |
25 | func ==(lhs: [T]?, rhs: [T]?) -> Bool {
26 | switch (lhs, rhs) {
27 | case (.some(let lhs), .some(let rhs)):
28 | return lhs == rhs
29 | case (.none, .none):
30 | return true
31 | default:
32 | return false
33 | }
34 | }
35 |
36 | func == (lhs: [[T]]?, rhs: [[T]]?) -> Bool {
37 | switch (lhs,rhs) {
38 | case (.some(let lhs), .some(let rhs)):
39 | return lhs.count == rhs.count && !zip(lhs, rhs).contains {$0 != $1 }
40 | case (.none, .none):
41 | return true
42 | default:
43 | return false
44 | }
45 | }
46 |
47 | func != (lhs: [[T]]?, rhs: [[T]]?) -> Bool {
48 | switch (lhs,rhs) {
49 | case (.some(let lhs), .some(let rhs)):
50 | return !(lhs.count == rhs.count && !zip(lhs, rhs).contains {$0 != $1 })
51 | case (.none, .none):
52 | return true
53 | default:
54 | return false
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Source/Core/Animation/Interpolatable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interpolatable.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/20/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol Interpolatable {
12 | static func interpolate(_ from: Self, _ to: Self, _ progress: Double) -> Self
13 | }
14 |
15 | extension CGFloat: Interpolatable {
16 | public static func interpolate(_ from: CGFloat, _ to: CGFloat, _ progress: Double) -> CGFloat {
17 | return (from + CGFloat(progress) * (to - from))
18 | }
19 | }
20 |
21 | extension Float: Interpolatable {
22 | public static func interpolate(_ from: Float, _ to: Float, _ progress: Double) -> Float {
23 | return (from + Float(progress) * (to - from))
24 | }
25 | }
26 |
27 | extension UIColor: Interpolatable {
28 | public static func interpolate(_ from: UIColor, _ to: UIColor, _ progress: Double) -> Self {
29 | var fromRed: CGFloat = 0, fromGreen: CGFloat = 0, fromBlue: CGFloat = 0, fromAlpha: CGFloat = 0
30 | from.getRed(&fromRed, green: &fromGreen, blue: &fromBlue, alpha: &fromAlpha)
31 |
32 | var toRed: CGFloat = 0, toGreen: CGFloat = 0, toBlue: CGFloat = 0, toAlpha: CGFloat = 0
33 | to.getRed(&toRed, green: &toGreen, blue: &toBlue, alpha: &toAlpha)
34 |
35 | let red = CGFloat.interpolate(fromRed, toRed, progress)
36 | let green = CGFloat.interpolate(fromGreen, toGreen, progress)
37 | let blue = CGFloat.interpolate(fromBlue, toBlue, progress)
38 | let alpha = CGFloat.interpolate(fromAlpha, toAlpha, progress)
39 |
40 | return self.init(red: red, green: green, blue: blue, alpha: alpha)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Source/Utilities/AsyncQueue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SerialOperationQueue.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 8/29/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | typealias Task = (@escaping () -> Void) -> Void
12 |
13 | class AsyncOperation: Operation {
14 | var task: Task?
15 |
16 | override var isAsynchronous: Bool {
17 | return false
18 | }
19 |
20 | override var isExecuting: Bool {
21 | return _executing
22 | }
23 |
24 | required override init() {}
25 |
26 | private var _executing = false {
27 | willSet {
28 | willChangeValue(forKey: "isExecuting")
29 | }
30 | didSet {
31 | didChangeValue(forKey: "isExecuting")
32 | }
33 | }
34 |
35 | override var isFinished: Bool {
36 | return _finished
37 | }
38 |
39 | private var _finished = false {
40 | willSet {
41 | willChangeValue(forKey: "isFinished")
42 | }
43 | didSet {
44 | didChangeValue(forKey: "isFinished")
45 | }
46 | }
47 |
48 | override func start() {
49 | _executing = true
50 |
51 | task? {
52 | self.complete()
53 | }
54 | }
55 |
56 | func complete() {
57 | _executing = false
58 | _finished = true
59 | }
60 | }
61 |
62 | class AsyncQueue: OperationQueue {
63 | init(maxConcurrentOperationCount: Int) {
64 | super.init()
65 |
66 | self.maxConcurrentOperationCount = maxConcurrentOperationCount
67 | }
68 |
69 | func enqueueOperation(withBlock block: @escaping Task) {
70 | let operation = OperationType()
71 | operation.task = block
72 | addOperation(operation)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Source/Template/NodeRegistry.swift:
--------------------------------------------------------------------------------
1 | public class NodeRegistry {
2 | public typealias ElementBuilder = ([String: Any], [Element]?) -> Element
3 | public static let shared = NodeRegistry()
4 |
5 | private lazy var elementBuilders = [String: ElementBuilder]()
6 |
7 | init() {
8 | registerDefaultProviders()
9 | }
10 |
11 | public func registerComponent(_ type: ComponentCreation.Type, _ propertiesType: T.Type) {
12 | registerElementBuilder("\(type)") { properties, children in
13 | return component(type, propertiesType.init(properties))
14 | }
15 | }
16 |
17 | public func registerElementBuilder(_ name: String, builder: @escaping ElementBuilder) {
18 | self.elementBuilders[name] = builder
19 | }
20 |
21 | func buildElement(with name: String, properties: [String: Any], children: [Element]?) -> Element {
22 | return elementBuilders[name]!(properties, children)
23 | }
24 |
25 | private func registerDefaultProviders() {
26 | registerElementBuilder("box") { properties, children in
27 | return box(DefaultProperties(properties), children)
28 | }
29 | registerElementBuilder("text") { properties, children in
30 | return text(TextProperties(properties))
31 | }
32 | registerElementBuilder("textfield") { properties, children in
33 | return textfield(TextFieldProperties(properties))
34 | }
35 | registerElementBuilder("image") { properties, children in
36 | return image(ImageProperties(properties))
37 | }
38 | registerElementBuilder("button") { properties, children in
39 | return button(ButtonProperties(properties))
40 | }
41 | registerElementBuilder("table") { properties, children in
42 | return table(TableProperties(properties))
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Source/iOS/DelegateProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DelegateProxy.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 8/9/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol DelegateProxyProtocol {
12 | init(target: AnyObject?, interceptor: NSObjectProtocol?)
13 | func registerInterceptable(selector: Selector)
14 | }
15 |
16 | class DelegateProxy: NSObject, DelegateProxyProtocol {
17 | let target: AnyObject?
18 | let interceptor: NSObjectProtocol?
19 | private lazy var selectors = Set()
20 |
21 | required init(target: AnyObject?, interceptor: NSObjectProtocol?) {
22 | self.target = target
23 | self.interceptor = interceptor
24 | }
25 |
26 | func registerInterceptable(selector: Selector) {
27 | selectors.insert(selector)
28 | }
29 |
30 | override func conforms(to aProtocol: Protocol) -> Bool {
31 | return true
32 | }
33 |
34 | override func responds(to aSelector: Selector) -> Bool {
35 | if intercepts(selector: aSelector) {
36 | return interceptor?.responds(to: aSelector) ?? false
37 | } else {
38 | return target?.responds(to: aSelector) ?? false
39 | }
40 | }
41 |
42 | override func forwardingTarget(for aSelector: Selector) -> Any? {
43 | if intercepts(selector: aSelector) {
44 | return interceptor
45 | } else if let target = target {
46 | return target.responds(to: aSelector) ? target : nil
47 | }
48 | return nil
49 | }
50 |
51 | private func intercepts(selector: Selector) -> Bool {
52 | return selectors.contains(selector)
53 | }
54 | }
55 |
56 | func ==(lhs: DelegateProxy, rhs: DelegateProxy) -> Bool {
57 | return lhs.target === rhs.target && lhs.interceptor === rhs.interceptor
58 | }
59 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Source/Core/Rendering.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Rendering.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/5/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func box(_ properties: DefaultProperties = DefaultProperties(), _ children: [Element]? = nil) -> Element {
12 | return ElementData(ElementType.box, properties, children)
13 | }
14 |
15 | public func image(_ properties: ImageProperties) -> Element {
16 | return ElementData(ElementType.image, properties)
17 | }
18 |
19 | public func text(_ properties: TextProperties) -> Element {
20 | return ElementData(ElementType.text, properties)
21 | }
22 |
23 | public func textfield(_ properties: TextFieldProperties) -> Element {
24 | return ElementData(ElementType.textField, properties)
25 | }
26 |
27 | public func button(_ properties: ButtonProperties) -> Element {
28 | return ElementData(ElementType.button, properties)
29 | }
30 |
31 | public func activityIndicator(_ properties: ActivityIndicatorProperties) -> Element {
32 | return ElementData(ElementType.activityIndicator, properties)
33 | }
34 |
35 | public func table(_ properties: TableProperties) -> Element {
36 | return ElementData(ElementType.table, properties)
37 | }
38 |
39 | public func collection(_ properties: CollectionProperties) -> Element {
40 | return ElementData(ElementType.collection, properties)
41 | }
42 |
43 |
44 | public func wrappedView(_ wrappedView: UIView, _ properties: DefaultProperties = DefaultProperties()) -> Element {
45 | return ElementData(ElementType.view(wrappedView), properties)
46 | }
47 |
48 | public func component(_ componentClass: ComponentCreation.Type, _ properties: T) -> Element {
49 | return ElementData(ElementType.component(componentClass), properties)
50 | }
51 |
--------------------------------------------------------------------------------
/Examples/Twitter/Source/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Source/Core/StyleSheet+Element.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StyleSheet+Element.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/26/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CSSParser
11 |
12 | public extension StyleElement where Self: Element {
13 | var parentElement: StyleElement? {
14 | return parent
15 | }
16 |
17 | var childElements: [StyleElement]? {
18 | return children
19 | }
20 | }
21 |
22 | extension ElementData: StyleElement {
23 | public var id: String? {
24 | return properties.core.identifier.id
25 | }
26 |
27 | public var classNames: [String]? {
28 | return properties.core.identifier.classNames
29 | }
30 |
31 | public var tagName: String? {
32 | return type.tagName
33 | }
34 |
35 | public var isFocused: Bool {
36 | if let focusable = properties as? FocusableProperties {
37 | return focusable.focused ?? false
38 | }
39 | return false
40 | }
41 |
42 | public var isEnabled: Bool {
43 | if let enableable = properties as? EnableableProperties {
44 | return enableable.enabled ?? false
45 | }
46 | return false
47 | }
48 |
49 | public var isActive: Bool {
50 | if let activatable = properties as? ActivatableProperties {
51 | return activatable.active ?? false
52 | }
53 | return false
54 | }
55 |
56 | public func has(attribute: String, with value: String) -> Bool {
57 | return properties.has(key: attribute, withValue: value)
58 | }
59 |
60 | public func equals(_ other: StyleElement) -> Bool {
61 | return equals(other as? Element)
62 | }
63 |
64 | public func apply(styles: [String : Any]) {
65 | var styledProperties = PropertiesType(styles)
66 | styledProperties.merge(properties)
67 | properties = styledProperties
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Examples/Twitter/Source/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Example
4 | //
5 | // Created by Matias Cudich on 8/7/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
17 | return true
18 | }
19 |
20 | func applicationWillResignActive(_ application: UIApplication) {
21 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
22 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
23 | }
24 |
25 | func applicationDidEnterBackground(_ application: UIApplication) {
26 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
27 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
28 | }
29 |
30 | func applicationWillEnterForeground(_ application: UIApplication) {
31 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
32 | }
33 |
34 | func applicationDidBecomeActive(_ application: UIApplication) {
35 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
36 | }
37 |
38 | func applicationWillTerminate(_ application: UIApplication) {
39 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
40 | }
41 |
42 |
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SimpleTable
4 | //
5 | // Created by Matias Cudich on 11/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Examples/Twitter/Source/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // TwitterClientExample
4 | //
5 | // Created by Matias Cudich on 10/27/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Source/Core/Element.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Element.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CSSParser
11 |
12 | public protocol ElementRepresentable {
13 | var tagName: String { get }
14 | func make(_ element: Element, _ owner: Node?, _ context: Context?) -> Node
15 | func equals(_ other: ElementRepresentable) -> Bool
16 | }
17 |
18 | public protocol Element: class, Keyable, StyleElement, ElementProvider {
19 | var type: ElementRepresentable { get }
20 | var children: [Element]? { get }
21 | weak var parent: Element? { get set }
22 |
23 | func build(withOwner owner: Node?, context: Context?) -> Node
24 | }
25 |
26 | extension ElementProvider where Self: Element {
27 | public func build(with model: Model) -> Element {
28 | return self
29 | }
30 | }
31 |
32 | public class ElementData: Element {
33 | public let type: ElementRepresentable
34 | public private(set) var children: [Element]?
35 | public weak var parent: Element?
36 | public var properties: PropertiesType
37 |
38 | public var key: String? {
39 | get {
40 | return properties.core.identifier.key
41 | }
42 | set {
43 | properties.core.identifier.key = newValue
44 | }
45 | }
46 |
47 | public init(_ type: ElementRepresentable, _ properties: PropertiesType, _ children: [Element]? = nil) {
48 | self.type = type
49 | self.properties = properties
50 | self.children = children
51 |
52 | for (index, child) in (self.children ?? []).enumerated() {
53 | self.children?[index].parent = self
54 | self.children?[index].key = child.key ?? "\(index)"
55 | }
56 | }
57 |
58 | public func build(withOwner owner: Node?, context: Context? = nil) -> Node {
59 | return type.make(self, owner, context)
60 | }
61 |
62 | public func equals(_ other: Element?) -> Bool {
63 | guard let other = other as? ElementData else {
64 | return false
65 | }
66 |
67 | return type.equals(other.type) && key == other.key && (parent?.equals(other.parent) ?? (other.parent == nil)) && properties == other.properties
68 | }
69 |
70 | public func equals(_ other: ElementProvider?) -> Bool {
71 | return equals(other as? Element)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Examples/SimpleTable/Source/App.swift:
--------------------------------------------------------------------------------
1 | //
2 | // App.swift
3 | // SimpleTable
4 | //
5 | // Created by Matias Cudich on 11/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | import TemplateKit
12 | import CSSLayout
13 |
14 | struct AppState: State {
15 | var items = [CollectionSection]()
16 | }
17 |
18 | func ==(lhs: AppState, rhs: AppState) -> Bool {
19 | return lhs.items.count == rhs.items.count
20 | }
21 |
22 | class App: Component {
23 |
24 | @objc func addSection() {
25 | updateState { state in
26 | state.items.append(CollectionSection(items: ["a"], hashValue: 1))
27 | }
28 | }
29 |
30 | @objc func removeSection() {
31 |
32 | }
33 |
34 | override func render() -> Template {
35 | var properties = DefaultProperties()
36 | properties.core.layout = self.properties.core.layout
37 |
38 | let tree = box(properties, [
39 | renderHeader(),
40 | renderItems()
41 | ])
42 |
43 | return Template(tree)
44 | }
45 |
46 | private func renderHeader() -> Element {
47 | return render(Bundle.main.url(forResource: "Header", withExtension: "xml")!).build(with: self)
48 | }
49 |
50 | private func renderItems() -> Element {
51 | var properties = CollectionProperties()
52 | properties.core.layout.flex = 1
53 | properties.core.style.backgroundColor = .white
54 | properties.collectionViewDataSource = self
55 | properties.items = state.items
56 |
57 | return collection(properties)
58 | }
59 |
60 | override func getInitialState() -> AppState {
61 | var state = AppState()
62 | state.items = [CollectionSection(items: ["1"], hashValue: 0)]
63 | return state
64 | }
65 | }
66 |
67 | extension App: CollectionViewDataSource {
68 | func collectionView(_ collectionView: CollectionView, elementAtIndexPath indexPath: IndexPath) -> Element {
69 | var properties = ItemProperties()
70 | properties.item = state.items[indexPath.section].items[indexPath.row] as? String
71 | properties.core.layout.width = self.properties.core.layout.width
72 | return component(Item.self, properties)
73 | }
74 |
75 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
76 | return state.items[section].items.count
77 | }
78 |
79 | func numberOfSections(in collectionView: UICollectionView) -> Int {
80 | return state.items.count
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Source/iOS/ActivityIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityIndicator.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/29/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct ActivityIndicatorProperties: Properties {
12 | public var core = CoreProperties()
13 |
14 | public var activityIndicatorViewStyle: UIActivityIndicatorViewStyle?
15 | public var hidesWhenStopped: Bool?
16 | public var color: UIColor?
17 |
18 | public init() {}
19 |
20 | public init(_ properties: [String : Any]) {
21 | core = CoreProperties(properties)
22 |
23 | activityIndicatorViewStyle = properties.cast("activityIndicatorViewStyle")
24 | hidesWhenStopped = properties.cast("hidesWhenStopped")
25 | color = properties.color("color")
26 | }
27 |
28 | public mutating func merge(_ other: ActivityIndicatorProperties) {
29 | core.merge(other.core)
30 |
31 | merge(&activityIndicatorViewStyle, other.activityIndicatorViewStyle)
32 | merge(&hidesWhenStopped, other.hidesWhenStopped)
33 | merge(&color, other.color)
34 | }
35 | }
36 |
37 | public func ==(lhs: ActivityIndicatorProperties, rhs: ActivityIndicatorProperties) -> Bool {
38 | return lhs.activityIndicatorViewStyle == rhs.activityIndicatorViewStyle && lhs.hidesWhenStopped == rhs.hidesWhenStopped && lhs.color == rhs.color && lhs.equals(otherProperties: rhs)
39 | }
40 |
41 | public class ActivityIndicator: UIActivityIndicatorView, NativeView {
42 | public weak var eventTarget: AnyObject?
43 | public lazy var eventRecognizers = EventRecognizers()
44 |
45 | public var properties = ActivityIndicatorProperties() {
46 | didSet {
47 | applyCoreProperties()
48 | applyActivityIndicatorViewProperties()
49 | }
50 | }
51 |
52 | public required init() {
53 | super.init(frame: CGRect.zero)
54 | }
55 |
56 | required public init(coder: NSCoder) {
57 | fatalError("init(coder:) has not been implemented")
58 | }
59 |
60 | private func applyActivityIndicatorViewProperties() {
61 | activityIndicatorViewStyle = properties.activityIndicatorViewStyle ?? .white
62 | hidesWhenStopped = properties.hidesWhenStopped ?? true
63 | color = properties.color
64 |
65 | if !isAnimating {
66 | startAnimating()
67 | }
68 | }
69 |
70 | public override func touchesBegan(_ touches: Set, with event: UIEvent?) {
71 | super.touchesBegan(touches, with: event)
72 |
73 | touchesBegan()
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Todos.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodosModel.swift
3 | // Example
4 | //
5 | // Created by Matias Cudich on 9/20/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct TodoItem: Hashable {
12 | var id: String = UUID().uuidString
13 | var title: String = ""
14 | var completed: Bool = false
15 |
16 | var hashValue: Int {
17 | var result = 17
18 |
19 | result = 31 * result + id.hashValue
20 |
21 | return result
22 | }
23 |
24 | init(title: String) {
25 | self.title = title
26 | }
27 | }
28 |
29 | func ==(lhs: TodoItem, rhs: TodoItem) -> Bool {
30 | return lhs.id == rhs.id && lhs.title == rhs.title && lhs.completed == rhs.completed
31 | }
32 |
33 | typealias ChangeHandler = () -> Void
34 |
35 | class Todos: Equatable {
36 | var todos = [TodoItem]()
37 | var changes = [ChangeHandler]()
38 |
39 | init() {}
40 |
41 | func subscribe(handler: @escaping ChangeHandler) {
42 | changes.append(handler)
43 | }
44 |
45 | func inform() {
46 | for change in changes {
47 | change()
48 | }
49 | }
50 |
51 | func addTodo(title: String) {
52 | todos.append(TodoItem(title: title))
53 | inform()
54 | }
55 |
56 | func toggleAll(checked: Bool) {
57 | for (index, _) in todos.enumerated() {
58 | todos[index].completed = checked
59 | }
60 | inform()
61 | }
62 |
63 | func toggle(id: String) {
64 | for (index, todo) in todos.enumerated() {
65 | if todo.id == id {
66 | todos[index].completed = !todo.completed
67 | break
68 | }
69 | }
70 | inform()
71 | }
72 |
73 | func move(from sourceIndex: Int, to destinationIndex: Int) {
74 | let todo = todos.remove(at: sourceIndex)
75 | todos.insert(todo, at: destinationIndex)
76 | inform()
77 | }
78 |
79 | func destroy(id: String) {
80 | todos = todos.filter { todoItem in
81 | todoItem.id != id
82 | }
83 | inform()
84 | }
85 |
86 | func save(id: String, title: String) {
87 | for (index, todo) in todos.enumerated() {
88 | if todo.id == id {
89 | todos[index].title = title
90 | break
91 | }
92 | }
93 | inform()
94 | }
95 |
96 | func clearCompleted() {
97 | todos = todos.filter { !$0.completed }
98 | inform()
99 | }
100 | }
101 |
102 | func ==(lhs: Todos, rhs: Todos) -> Bool {
103 | return lhs.todos == rhs.todos
104 | }
105 |
--------------------------------------------------------------------------------
/Source/iOS/Image.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Image.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct ImageProperties: Properties {
12 | public var core = CoreProperties()
13 |
14 | public var contentMode: UIViewContentMode?
15 | public var url: URL?
16 | public var name: String?
17 | public var image: UIImage?
18 |
19 | public init() {}
20 |
21 | public init(_ properties: [String : Any]) {
22 | core = CoreProperties(properties)
23 |
24 | contentMode = properties.cast("contentMode")
25 | url = properties.cast("url")
26 | name = properties.cast("name")
27 | image = properties.image("image")
28 | }
29 |
30 | public mutating func merge(_ other: ImageProperties) {
31 | core.merge(other.core)
32 |
33 | merge(&contentMode, other.contentMode)
34 | merge(&url, other.url)
35 | merge(&name, other.name)
36 | merge(&image, other.image)
37 | }
38 | }
39 |
40 | public func ==(lhs: ImageProperties, rhs: ImageProperties) -> Bool {
41 | return lhs.contentMode == rhs.contentMode && lhs.url == rhs.url && lhs.name == rhs.name && lhs.equals(otherProperties: rhs)
42 | }
43 |
44 | public class Image: UIImageView, NativeView {
45 | public weak var eventTarget: AnyObject?
46 | public lazy var eventRecognizers = EventRecognizers()
47 |
48 | public var properties = ImageProperties() {
49 | didSet {
50 | applyCoreProperties()
51 | applyImageProperties()
52 | }
53 | }
54 |
55 | public required init() {
56 | super.init(frame: CGRect.zero)
57 | }
58 |
59 | public required init?(coder aDecoder: NSCoder) {
60 | fatalError("init(coder:) has not been implemented")
61 | }
62 |
63 | private func applyImageProperties() {
64 | contentMode = properties.contentMode ?? .scaleAspectFit
65 |
66 | if let url = properties.url {
67 | ImageService.shared.load(url) { [weak self] result in
68 | switch result {
69 | case .success(let image):
70 | DispatchQueue.main.async {
71 | self?.image = image
72 | }
73 | case .failure(_):
74 | // TODO(mcudich): Show placeholder error image.
75 | break
76 | }
77 | }
78 | } else if let name = properties.name {
79 | self.image = UIImage(named: name)
80 | } else if let image = properties.image {
81 | self.image = image
82 | }
83 | }
84 |
85 | public override func touchesBegan(_ touches: Set, with event: UIEvent?) {
86 | super.touchesBegan(touches, with: event)
87 |
88 | touchesBegan()
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Footer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Footer.swift
3 | // Example
4 | //
5 | // Created by Matias Cudich on 9/22/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import TemplateKit
11 |
12 | struct FooterProperties: Properties {
13 | var core = CoreProperties()
14 |
15 | var count: Int?
16 | var completedCount: Int?
17 | var onClearCompleted: Selector?
18 | var onUpdateFilter: Selector?
19 | var nowShowing: Filter?
20 |
21 | public init() {}
22 |
23 | public init(_ properties: [String : Any]) {
24 | core = CoreProperties(properties)
25 |
26 | count = properties.cast("count") ?? 0
27 | completedCount = properties.cast("completedCount") ?? 0
28 | onClearCompleted = properties.cast("onClearCompleted")
29 | nowShowing = properties.get("nowShowing")
30 | onUpdateFilter = properties.cast("onUpdateFilter")
31 | }
32 |
33 | mutating func merge(_ other: FooterProperties) {
34 | core.merge(other.core)
35 |
36 | merge(&count, other.count)
37 | merge(&completedCount, other.completedCount)
38 | merge(&onClearCompleted, other.onClearCompleted)
39 | merge(&nowShowing, other.nowShowing)
40 | merge(&onUpdateFilter, other.onUpdateFilter)
41 | }
42 | }
43 |
44 | func ==(lhs: FooterProperties, rhs: FooterProperties) -> Bool {
45 | return lhs.count == rhs.count && lhs.completedCount == rhs.completedCount && lhs.onClearCompleted == rhs.onClearCompleted && lhs.nowShowing == rhs.nowShowing && lhs.onUpdateFilter == rhs.onUpdateFilter
46 | }
47 |
48 | class Footer: Component {
49 | static let templateURL = Bundle.main.url(forResource: "Footer", withExtension: "xml")!
50 |
51 | var count: String?
52 | var allSelected = false
53 | var activeSelected = false
54 | var completedSelected = false
55 |
56 | @objc func handleSelectAll() {
57 | performSelector(properties.onUpdateFilter, with: Filter.all.rawValue)
58 | }
59 |
60 | @objc func handleSelectActive() {
61 | performSelector(properties.onUpdateFilter, with: Filter.active.rawValue)
62 | }
63 |
64 | @objc func handleSelectCompleted() {
65 | performSelector(properties.onUpdateFilter, with: Filter.completed.rawValue)
66 | }
67 |
68 | @objc func handleClearCompleted() {
69 | performSelector(properties.onClearCompleted)
70 | }
71 |
72 | override func render() -> Template {
73 | count = "\(properties.completedCount ?? 0) items completed"
74 | allSelected = properties.nowShowing == .all
75 | activeSelected = properties.nowShowing == .active
76 | completedSelected = properties.nowShowing == .completed
77 |
78 | return render(Footer.templateURL)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Source/Core/Animation/Animatable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Animatable.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/18/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum AnimationState {
12 | case pending
13 | case running
14 | case done
15 | }
16 |
17 | public protocol Tickable {
18 | var hashValue: Int { get }
19 | var state: AnimationState { get }
20 |
21 | func tick(time: TimeInterval)
22 | func equals(_ other: Tickable) -> Bool
23 | }
24 |
25 | public class Animatable: Tickable, Model, Equatable {
26 | public var hashValue: Int {
27 | return ObjectIdentifier(self as AnyObject).hashValue
28 | }
29 |
30 | public private(set) var value: T?
31 | public var duration: TimeInterval = 0
32 | public var delay: TimeInterval = 0
33 | public var interpolator: Interpolator
34 |
35 | public var state: AnimationState {
36 | if toValue != nil && beginTime == nil {
37 | return .pending
38 | } else if beginTime != nil {
39 | return .running
40 | }
41 | return .done
42 | }
43 |
44 | private var fromValue: T?
45 | private var toValue: T?
46 | private var beginTime: TimeInterval?
47 |
48 | public init(_ value: T?, duration: TimeInterval = 0, delay: TimeInterval = 0, interpolator: Interpolator = BezierInterpolator(.linear)) {
49 | self.value = value
50 | self.duration = duration
51 | self.delay = delay
52 | self.interpolator = interpolator
53 | }
54 |
55 | public func set(_ value: T?) {
56 | if value != self.value && duration > 0 {
57 | fromValue = self.value
58 | toValue = value
59 | return
60 | }
61 |
62 | self.value = value
63 | }
64 |
65 | public func tick(time: TimeInterval) {
66 | if beginTime == nil {
67 | beginTime = time
68 | }
69 | guard let beginTime = beginTime, let fromValue = fromValue, let toValue = toValue, time >= beginTime + delay else {
70 | return
71 | }
72 |
73 | let effectiveBeginTime = beginTime + delay
74 | if time - effectiveBeginTime > duration {
75 | reset()
76 | return
77 | }
78 |
79 | value = interpolator.interpolate(fromValue, toValue, time - effectiveBeginTime, duration)
80 | }
81 |
82 | public func reset() {
83 | beginTime = nil
84 | toValue = nil
85 | fromValue = nil
86 | }
87 |
88 | public func equals(_ other: Tickable) -> Bool {
89 | guard let other = other as? Animatable else {
90 | return false
91 | }
92 | return self == other
93 | }
94 | }
95 |
96 | public func ==(lhs: Animatable, rhs: Animatable) -> Bool {
97 | return lhs.value == rhs.value
98 | }
99 |
--------------------------------------------------------------------------------
/Examples/Twitter/Source/App.swift:
--------------------------------------------------------------------------------
1 | //
2 | // App.swift
3 | // TwitterClientExample
4 | //
5 | // Created by Matias Cudich on 10/27/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | import TemplateKit
12 | import CSSLayout
13 |
14 | struct AppState: State {
15 | var tweets = [Tweet]()
16 | }
17 |
18 | func ==(lhs: AppState, rhs: AppState) -> Bool {
19 | return lhs.tweets == rhs.tweets
20 | }
21 |
22 | class App: Component {
23 | override func didBuild() {
24 | TwitterClient.shared.fetchSearchResultsWithQuery(query: "donald trump") { tweets in
25 | self.updateState { state in
26 | state.tweets = tweets
27 | }
28 | }
29 | }
30 |
31 | override func render() -> Template {
32 | var properties = DefaultProperties()
33 | properties.core.layout = self.properties.core.layout
34 |
35 | var tree: Element!
36 | if state.tweets.count > 0 {
37 | tree = box(properties, [
38 | renderTweets()
39 | ])
40 | } else {
41 | properties.core.layout.alignItems = CSSAlignCenter
42 | properties.core.layout.justifyContent = CSSJustifyCenter
43 | tree = box(properties, [
44 | activityIndicator(ActivityIndicatorProperties(["activityIndicatorViewStyle": UIActivityIndicatorViewStyle.gray]))
45 | ])
46 | }
47 |
48 | return Template(tree)
49 | }
50 |
51 | @objc func handleEndReached() {
52 | guard let maxId = state.tweets.last?.id else {
53 | return
54 | }
55 | TwitterClient.shared.fetchSearchResultsWithQuery(query: "donald trump", maxId: maxId) { tweets in
56 | self.updateState { state in
57 | state.tweets.append(contentsOf: tweets.dropFirst())
58 | }
59 | }
60 | }
61 |
62 | private func renderTweets() -> Element {
63 | var properties = TableProperties()
64 | properties.core.layout.flex = 1
65 | properties.tableViewDataSource = self
66 | properties.items = [TableSection(items: state.tweets, hashValue: 0)]
67 | properties.onEndReached = #selector(App.handleEndReached)
68 | properties.onEndReachedThreshold = 700
69 |
70 | return table(properties)
71 | }
72 | }
73 |
74 | extension App: TableViewDataSource {
75 | func tableView(_ tableView: TableView, elementAtIndexPath indexPath: IndexPath) -> Element {
76 | var properties = TweetProperties()
77 | properties.tweet = state.tweets[indexPath.row]
78 | properties.core.layout.width = self.properties.core.layout.width
79 | return component(TweetItem.self, properties)
80 | }
81 |
82 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
83 | return state.tweets.count
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Source/Core/Animation/Animator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Animator.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/19/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol AnimatorObserver {
12 | var hashValue: Int { get }
13 | func didAnimate()
14 | func equals(_ other: AnimatorObserver) -> Bool
15 | }
16 |
17 | struct TickableKey: Hashable {
18 | let tickable: Tickable
19 |
20 | var hashValue: Int {
21 | return tickable.hashValue
22 | }
23 | }
24 |
25 | func ==(lhs: TickableKey, rhs: TickableKey) -> Bool {
26 | return lhs.tickable.equals(rhs.tickable)
27 | }
28 |
29 | struct AnimatorObserverValue: Hashable {
30 | let animatorObserver: AnimatorObserver
31 |
32 | var hashValue: Int {
33 | return animatorObserver.hashValue
34 | }
35 | }
36 |
37 | func ==(lhs: AnimatorObserverValue, rhs: AnimatorObserverValue) -> Bool {
38 | return lhs.animatorObserver.equals(rhs.animatorObserver)
39 | }
40 |
41 | public class Animator {
42 | public static let shared = Animator()
43 |
44 | private lazy var animatables = [Tickable]()
45 | private lazy var observers = [TickableKey: AnimatorObserverValue]()
46 |
47 | private lazy var displayLink: CADisplayLink = {
48 | let displayLink = CADisplayLink(target: self, selector: #selector(Animator.render))
49 | displayLink.isPaused = true
50 | displayLink.add(to: RunLoop.main, forMode: .commonModes)
51 | return displayLink
52 | }()
53 |
54 | public func addAnimatable(_ animatable: Tickable) {
55 | animatables.append(animatable)
56 | updateDisplayLink()
57 | }
58 |
59 | public func addObserver(_ observer: T, for animatable: Tickable) {
60 | let key = TickableKey(tickable: animatable)
61 | observers[key] = AnimatorObserverValue(animatorObserver: observer)
62 | }
63 |
64 | @objc private func render() {
65 | var validObservers = Set()
66 | for animatable in animatables {
67 | animatable.tick(time: CACurrentMediaTime())
68 | if let observer = observers[TickableKey(tickable: animatable)] {
69 | validObservers.insert(observer)
70 | }
71 | }
72 |
73 | for observer in validObservers {
74 | observer.animatorObserver.didAnimate()
75 | }
76 |
77 | animatables = animatables.reduce([]) { accum, value in
78 | if value.state == .running {
79 | return accum + [value]
80 | } else {
81 | observers.removeValue(forKey: TickableKey(tickable: value))
82 | }
83 |
84 | return accum
85 | }
86 |
87 | updateDisplayLink()
88 | }
89 |
90 | private func updateDisplayLink() {
91 | let pause = animatables.count == 0
92 | displayLink.isPaused = pause
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/TemplateKit.xcworkspace/xcshareddata/TemplateKit.xcscmblueprint:
--------------------------------------------------------------------------------
1 | {
2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "07678C7AA711A45622B474B31CFF27C4316A148A",
3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : {
4 |
5 | },
6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : {
7 | "2805B76DDB6FB3AFC1EC29F779C674810C8B5895" : 9223372036854775807,
8 | "4F71518F0B0C69061FD45C252C55A2FA5AE9364E" : 0,
9 | "07678C7AA711A45622B474B31CFF27C4316A148A" : 0,
10 | "EF4E0D78BD66D72DA5A8D5BC350B60E01C5A9EF4" : 9223372036854775807
11 | },
12 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "6FABF1C1-9959-4353-B644-5B4CF302F3AE",
13 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : {
14 | "2805B76DDB6FB3AFC1EC29F779C674810C8B5895" : "TemplateKit\/ThirdParty\/katana-parser\/",
15 | "4F71518F0B0C69061FD45C252C55A2FA5AE9364E" : "SwiftBox\/",
16 | "07678C7AA711A45622B474B31CFF27C4316A148A" : "TemplateKit\/",
17 | "EF4E0D78BD66D72DA5A8D5BC350B60E01C5A9EF4" : "TemplateKit\/ThirdParty\/css-layout\/"
18 | },
19 | "DVTSourceControlWorkspaceBlueprintNameKey" : "TemplateKit",
20 | "DVTSourceControlWorkspaceBlueprintVersion" : 204,
21 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "TemplateKit.xcworkspace",
22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [
23 | {
24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/mcudich\/TemplateKit.git",
25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "07678C7AA711A45622B474B31CFF27C4316A148A"
27 | },
28 | {
29 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/hackers-painters\/katana-parser",
30 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
31 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "2805B76DDB6FB3AFC1EC29F779C674810C8B5895"
32 | },
33 | {
34 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/mcudich\/SwiftBox.git",
35 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
36 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "4F71518F0B0C69061FD45C252C55A2FA5AE9364E"
37 | },
38 | {
39 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/facebook\/css-layout.git",
40 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
41 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "EF4E0D78BD66D72DA5A8D5BC350B60E01C5A9EF4"
42 | }
43 | ]
44 | }
--------------------------------------------------------------------------------
/Source/Utilities/XMLDocument.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XMLDocument.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/25/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class XMLElement: Equatable {
12 | var name: String
13 | var value: String?
14 | weak var parent: XMLElement?
15 | lazy var children = [XMLElement]()
16 | lazy var attributes = [String: String]()
17 |
18 | init(name: String, attributes: [String: String]) {
19 | self.name = name
20 | self.attributes = attributes
21 | }
22 |
23 | func addChild(name: String, attributes: [String: String]) -> XMLElement {
24 | let element = XMLElement(name: name, attributes: attributes)
25 | element.parent = self
26 | children.append(element)
27 | return element
28 | }
29 | }
30 |
31 | func ==(lhs: XMLElement, rhs: XMLElement) -> Bool {
32 | return lhs.name == rhs.name && lhs.attributes == rhs.attributes && lhs.children == rhs.children
33 | }
34 |
35 | enum XMLError: Error {
36 | case parserError(String)
37 | }
38 |
39 | class XMLDocument: NSObject, XMLParserDelegate {
40 | let data: Data
41 | private(set) var root: XMLElement?
42 |
43 | private var currentParent: XMLElement?
44 | private var currentElement: XMLElement?
45 | private var currentValue = ""
46 | private var parseError: Error?
47 |
48 | init(data: Data) throws {
49 | self.data = data
50 |
51 | super.init()
52 |
53 | try parse()
54 | }
55 |
56 | private func parse() throws {
57 | let parser = XMLParser(data: data)
58 | parser.delegate = self
59 |
60 | guard parser.parse() else {
61 | guard let error = parseError else {
62 | throw XMLError.parserError("Failure parsing: \(parseError?.localizedDescription)")
63 | }
64 | throw error
65 | }
66 | }
67 |
68 | @objc func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
69 | currentElement = currentParent?.addChild(name: elementName, attributes: attributeDict) ?? XMLElement(name: elementName, attributes: attributeDict)
70 | if root == nil {
71 | root = currentElement!
72 | }
73 | currentParent = currentElement
74 | }
75 |
76 | @objc func parser(_ parser: XMLParser, foundCharacters string: String) {
77 | currentValue += string
78 | let newValue = currentValue.trimmingCharacters(in: .whitespacesAndNewlines)
79 | currentElement?.value = newValue.isEmpty ? nil : newValue
80 | }
81 |
82 | @objc func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
83 | currentParent = currentParent?.parent
84 | currentElement = nil
85 | currentValue = ""
86 | }
87 |
88 | @objc func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
89 | self.parseError = parseError
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Source/Utilities/ResourceService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkService.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 8/23/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | typealias CompletionHandler = (Result) -> Void
12 |
13 | protocol Parser {
14 | associatedtype ParsedType
15 |
16 | init()
17 | func parse(data: Data) throws -> ParsedType
18 | }
19 |
20 | public enum CachePolicy {
21 | case always
22 | case never
23 | }
24 |
25 | class ResourceService {
26 | typealias ResponseType = ParserType.ParsedType
27 |
28 | public var cachePolicy: CachePolicy = .always
29 |
30 | private lazy var defaultSession = URLSession(configuration: .default)
31 | private lazy var operationQueue = AsyncQueue(maxConcurrentOperationCount: 8)
32 | private lazy var pendingOperations = [URL: [CompletionHandler]]()
33 | private lazy var cache = [URL: ResponseType]()
34 |
35 | private let requestQueue: DispatchQueue
36 |
37 | init(requestQueue: DispatchQueue) {
38 | self.requestQueue = requestQueue
39 | }
40 |
41 | func load(_ url: URL, completion: @escaping CompletionHandler) {
42 | requestQueue.async { [weak self] in
43 | self?.enqueueLoad(url, completion: completion)
44 | }
45 | }
46 |
47 | func enqueueLoad(_ url: URL, completion: @escaping CompletionHandler) {
48 | if let response = cache[url] {
49 | return completion(.success(response))
50 | }
51 |
52 | if pendingOperations[url] != nil {
53 | pendingOperations[url]?.append(completion)
54 | return
55 | }
56 |
57 | pendingOperations[url] = [completion]
58 |
59 | operationQueue.enqueueOperation { [weak self] done in
60 | self?.defaultSession.dataTask(with: url) { [weak self] data, response, error in
61 | self?.requestQueue.async {
62 | if let data = data {
63 | self?.processResponse(forURL: url, withData: data)
64 | } else if let error = error {
65 | self?.fail(forURL: url, withError: error)
66 | }
67 | _ = self?.pendingOperations.removeValue(forKey: url)
68 | done()
69 | }
70 | }.resume()
71 | }
72 | }
73 |
74 | private func processResponse(forURL url: URL, withData data: Data) {
75 | let parser = ParserType()
76 | do {
77 | let parsed = try parser.parse(data: data)
78 | if cachePolicy == .always {
79 | cache[url] = parsed
80 | }
81 | processCallbacks(forURL: url, result: .success(parsed))
82 | } catch {
83 | fail(forURL: url, withError: error)
84 | }
85 | }
86 |
87 | private func fail(forURL url: URL, withError error: Error) {
88 | processCallbacks(forURL: url, result: .failure(error))
89 | }
90 |
91 | private func processCallbacks(forURL url: URL, result: Result) {
92 | guard let pendingCallbacks = pendingOperations[url] else {
93 | return
94 | }
95 | pendingCallbacks.forEach { $0(result) }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Source/Template/Model.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol Model {
4 | func value(forKey key: String) -> T?
5 | func value(forKeyPath keyPath: String) -> T?
6 | subscript (key : String) -> Any? { get }
7 | }
8 |
9 | public extension Model {
10 | func value(forKey key: String) -> T? {
11 | var currentMirror: Mirror? = Mirror(reflecting: self)
12 | while let mirror = currentMirror {
13 | for child in mirror.children {
14 | if keysEqual(child.label, key: key) {
15 | return unwrapAny(child.value) as? T
16 | }
17 | }
18 | currentMirror = mirror.superclassMirror
19 | }
20 | return nil
21 | }
22 |
23 | func value(forKeyPath keyPath: String) -> T? {
24 | var currentMirror: Mirror? = Mirror(reflecting: self)
25 | while var mirror = currentMirror {
26 | let keys = keyPath.components(separatedBy: ".")
27 | for key in keys {
28 | for child in mirror.children {
29 | if keysEqual(child.label, key: key) {
30 | if keysEqual(child.label, key: keys.last) {
31 | return child.value as? T
32 | } else {
33 | mirror = Mirror(reflecting: unwrapAny(child.value))
34 | break
35 | }
36 | }
37 | }
38 | }
39 | currentMirror = mirror.superclassMirror
40 | }
41 |
42 | return nil
43 | }
44 |
45 | subscript(key: String) -> Any? {
46 | get {
47 | return self.value(forKeyPath: key)
48 | }
49 | }
50 |
51 | func resolve(properties: [String: String]) -> [String: Any] {
52 | var resolvedProperties = [String: Any]()
53 | for (key, value) in properties {
54 | resolvedProperties[key] = resolve(value)
55 | }
56 |
57 | return resolvedProperties
58 | }
59 |
60 | func resolve(_ value: Any) -> Any? {
61 | guard let expression = value as? String, expression.hasPrefix("$") else {
62 | return value
63 | }
64 |
65 | let startIndex = expression.characters.index(expression.startIndex, offsetBy: 1)
66 | let keyPath = expression.substring(from: startIndex)
67 |
68 | return self.value(forKeyPath: keyPath)
69 | }
70 |
71 | private func keysEqual(_ childLabel: String?, key: String?) -> Bool {
72 | return childLabel?.replacingOccurrences(of: ".storage", with: "") == key
73 | }
74 |
75 | private func unwrapAny(_ val: Any) -> Any {
76 | let mirror = Mirror(reflecting: val)
77 | if mirror.displayStyle == .optional {
78 | for child in mirror.children {
79 | if child.label == "some" {
80 | return child.value
81 | }
82 | }
83 | }
84 | return val
85 | }
86 | }
87 |
88 | extension Dictionary: Model {}
89 |
90 | extension Dictionary {
91 | public func value(forKey key: String) -> T? {
92 | let separator = "."
93 | var keyPath = key.components(separatedBy: separator)
94 |
95 | guard let stringKey = keyPath.removeFirst() as? Key else {
96 | fatalError("Attempting to fetch value from dictionary without string keys")
97 | }
98 |
99 | let rootValue = self[stringKey] as? T
100 |
101 | if keyPath.count > 0, let modelValue = rootValue as? Model {
102 | return modelValue.value(forKeyPath: keyPath.joined(separator: separator))
103 | }
104 | return rootValue
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/Source/Todo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodoItem.swift
3 | // Example
4 | //
5 | // Created by Matias Cudich on 9/20/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import TemplateKit
11 |
12 | struct TodoState: State {
13 | var editText: String?
14 | }
15 |
16 | func ==(lhs: TodoState, rhs: TodoState) -> Bool {
17 | return lhs.editText == rhs.editText
18 | }
19 |
20 | struct TodoProperties: Properties {
21 | var core = CoreProperties()
22 |
23 | var todo: TodoItem?
24 | var editing: Bool?
25 | var onToggle: Selector?
26 | var onDestroy: Selector?
27 | var onEdit: Selector?
28 | var onSave: Selector?
29 | var onCancel: Selector?
30 |
31 | public init() {}
32 |
33 | public init(_ properties: [String : Any]) {
34 | core = CoreProperties(properties)
35 |
36 | todo = properties.get("todo")
37 | editing = properties.cast("editing") ?? false
38 | onToggle = properties.cast("onToggle")
39 | onDestroy = properties.cast("onDestroy")
40 | onEdit = properties.cast("onEdit(")
41 | onSave = properties.cast("onSave")
42 | onCancel = properties.cast("onCancel")
43 | }
44 |
45 | mutating func merge(_ other: TodoProperties) {
46 | core.merge(other.core)
47 |
48 | merge(&todo, other.todo)
49 | merge(&editing, other.editing)
50 | merge(&onToggle, other.onToggle)
51 | merge(&onDestroy, other.onDestroy)
52 | merge(&onEdit, other.onEdit)
53 | merge(&onSave, other.onSave)
54 | merge(&onCancel, other.onCancel)
55 | }
56 | }
57 |
58 | func ==(lhs: TodoProperties, rhs: TodoProperties) -> Bool {
59 | return lhs.todo == rhs.todo && lhs.editing == rhs.editing && lhs.onToggle == rhs.onToggle && lhs.onDestroy == rhs.onDestroy && lhs.onEdit == rhs.onEdit && lhs.onSave == rhs.onSave && lhs.onCancel == rhs.onCancel
60 | }
61 |
62 | class Todo: Component {
63 | static let templateURL = Bundle.main.url(forResource: "Todo", withExtension: "xml")!
64 |
65 | var buttonBackgroundColor: UIColor?
66 | var text: String?
67 | var enabled: Bool?
68 |
69 | @objc func handleSubmit(target: UITextField) {
70 | guard let todo = properties.todo else { return }
71 |
72 | if let text = target.text, !text.isEmpty {
73 | performSelector(properties.onSave, with: todo.id, with: text)
74 | updateState { state in
75 | state.editText = nil
76 | }
77 | } else {
78 | performSelector(properties.onDestroy, with: todo.id)
79 | }
80 | }
81 |
82 | @objc func handleEdit() {
83 | guard let todo = properties.todo else { return }
84 |
85 | performSelector(properties.onEdit, with: todo.id)
86 | updateState { state in
87 | state.editText = todo.title
88 | }
89 | }
90 |
91 | @objc func handleChange(target: UITextField) {
92 | if properties.editing ?? false {
93 | updateState { state in
94 | state.editText = target.text
95 | }
96 | }
97 | }
98 |
99 | @objc func handleToggle() {
100 | performSelector(properties.onToggle, with: properties.todo?.id)
101 | }
102 |
103 | @objc func handleDestroy() {
104 | performSelector(properties.onDestroy, with: properties.todo?.id)
105 | }
106 |
107 | override func render() -> Template {
108 | buttonBackgroundColor = (self.properties.todo?.completed ?? false) ? UIColor.green : UIColor.red
109 | enabled = state.editText != nil
110 | text = state.editText ?? self.properties.todo?.title
111 |
112 | return render(Todo.templateURL)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Examples/TodoMVC/TodoMVC.xcodeproj/xcshareddata/xcschemes/TodoMVC.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Examples/Twitter/Twitter.xcodeproj/xcshareddata/xcschemes/Twitter.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/TemplateKit.xcodeproj/xcshareddata/xcschemes/TemplateKitTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
55 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
74 |
80 |
81 |
82 |
83 |
85 |
86 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/Source/iOS/UIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/9/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension UIView: View {
12 | public var children: [View] {
13 | get {
14 | return subviews
15 | }
16 | set {
17 | var viewsToRemove = Set(subviews)
18 |
19 | for (index, child) in newValue.enumerated() {
20 | let childView = child as! UIView
21 | if (subviews.count > index && childView !== subviews[index]) || index >= subviews.count {
22 | insertSubview(childView, at: index)
23 | }
24 | viewsToRemove.remove(childView)
25 | }
26 |
27 | viewsToRemove.forEach { viewToRemove in
28 | viewToRemove.removeFromSuperview()
29 | }
30 | }
31 | }
32 |
33 | public var parent: View? {
34 | return superview as? View
35 | }
36 |
37 | public func add(_ view: View) {
38 | addSubview(view as! UIView)
39 | }
40 |
41 | public func replace(_ view: View, with newView: View) {
42 | guard let currentIndex = subviews.index(of: view as! UIView) else {
43 | return
44 | }
45 | (view as! UIView).removeFromSuperview()
46 | insertSubview(newView as! UIView, at: currentIndex)
47 | }
48 | }
49 |
50 | extension NativeView where Self: UIView {
51 | func applyCoreProperties() {
52 | applyBackgroundColor()
53 | applyBorder()
54 | applyCornerRadius()
55 | applyOpacity()
56 | applyTapHandler()
57 | }
58 |
59 | private func applyBackgroundColor() {
60 | backgroundColor = properties.core.style.backgroundColor
61 | }
62 |
63 | private func applyBorder() {
64 | layer.borderColor = properties.core.style.borderColor?.cgColor
65 |
66 | if layer.borderColor != nil {
67 | layer.borderWidth = (properties.core.style.borderWidth ?? 1) / UIScreen.main.scale
68 | }
69 | }
70 |
71 | private func applyCornerRadius() {
72 | layer.cornerRadius = properties.core.style.cornerRadius ?? 0
73 | layer.masksToBounds = layer.cornerRadius > 0
74 | }
75 |
76 | private func applyOpacity() {
77 | alpha = properties.core.style.opacity ?? 1
78 | }
79 |
80 | private func applyTapHandler() {
81 | let singleTap = updateTapGestureRecognizer(recognizer: &eventRecognizers.onTap, selector: properties.core.gestures.onTap, numberOfTaps: 1)
82 | if let doubleTap = updateTapGestureRecognizer(recognizer: &eventRecognizers.onDoubleTap, selector: properties.core.gestures.onDoubleTap, numberOfTaps: 2) {
83 | singleTap?.require(toFail: doubleTap)
84 | }
85 | }
86 |
87 | private func updateTapGestureRecognizer(recognizer: inout EventRecognizers.Recognizer?, selector: Selector?, numberOfTaps: Int) -> UIGestureRecognizer? {
88 | if let existingRecognizer = recognizer, let selector = selector, selector != existingRecognizer.0 {
89 | existingRecognizer.1.removeTarget(eventTarget as Any, action: existingRecognizer.0)
90 | existingRecognizer.1.addTarget(eventTarget as Any, action: selector)
91 | recognizer = (selector, existingRecognizer.1)
92 | } else if let selector = selector {
93 | let newRecognizer = UITapGestureRecognizer(target: eventTarget, action: selector)
94 | newRecognizer.numberOfTapsRequired = numberOfTaps
95 | addGestureRecognizer(newRecognizer)
96 | recognizer = (selector, newRecognizer)
97 | return newRecognizer
98 | } else if let existingRecognizer = recognizer {
99 | removeGestureRecognizer(existingRecognizer.1)
100 | recognizer = nil
101 | }
102 | return nil
103 | }
104 |
105 | public func touchesBegan() {
106 | if let onPress = properties.core.gestures.onPress {
107 | _ = eventTarget?.perform(onPress)
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Tests/DiffTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DiffTests.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/30/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import TemplateKit
11 |
12 | struct FakeItem: Hashable {
13 | let value: Int
14 | let eValue: Int
15 |
16 | var hashValue: Int {
17 | return value.hashValue
18 | }
19 | }
20 |
21 | func ==(lhs: FakeItem, rhs: FakeItem) -> Bool {
22 | return lhs.eValue == rhs.eValue
23 | }
24 |
25 | func ==(lhs: (from: Int, to: Int), rhs: (from: Int, to: Int)) -> Bool {
26 | return lhs.0 == rhs.0 && lhs.1 == rhs.1
27 | }
28 |
29 | class DiffTests: XCTestCase {
30 | func testEmptyArrays() {
31 | let o = [Int]()
32 | let n = [Int]()
33 | let result = diff(o, n)
34 | XCTAssertEqual(0, result.count)
35 | }
36 |
37 | func testDiffingFromEmptyArray() {
38 | let o = [Int]()
39 | let n = [1]
40 | let result = diff(o, n)
41 | XCTAssertEqual(.insert(0), result[0])
42 | XCTAssertEqual(1, result.count)
43 | }
44 |
45 | func testDiffingToEmptyArray() {
46 | let o = [1]
47 | let n = [Int]()
48 | let result = diff(o, n)
49 | XCTAssertEqual(.delete(0), result[0])
50 | XCTAssertEqual(1, result.count)
51 | }
52 |
53 | func testSwapHasMoves() {
54 | let o = [1, 2, 3]
55 | let n = [2, 3, 1]
56 | let result = diff(o, n)
57 | XCTAssertEqual([.delete(2), .delete(1), .delete(0), .insert(0), .insert(1), .insert(2)], result)
58 | }
59 |
60 | func testMovingTogether() {
61 | let o = [1, 2, 3, 3, 4]
62 | let n = [2, 3, 1, 3, 4]
63 | let result = diff(o, n)
64 | XCTAssertEqual([.delete(2), .delete(1), .delete(0), .insert(0), .insert(1), .insert(2)], result)
65 | }
66 |
67 | func testSwappedValuesHaveMoves() {
68 | let o = [1, 2, 3, 4]
69 | let n = [2, 4, 5, 3]
70 | let result = diff(o, n)
71 | XCTAssertEqual([.delete(3), .delete(2), .delete(0), .insert(1), .insert(2), .insert(3)], result)
72 | }
73 |
74 | func testUpdates() {
75 | let o = [
76 | FakeItem(value: 0, eValue: 0),
77 | FakeItem(value: 1, eValue: 1),
78 | FakeItem(value: 2, eValue: 2)
79 | ]
80 | let n = [
81 | FakeItem(value: 0, eValue: 1),
82 | FakeItem(value: 1, eValue: 2),
83 | FakeItem(value: 2, eValue: 3)
84 | ]
85 | let result = diff(o, n)
86 | XCTAssertEqual([.update(0), .update(1), .update(2)], result)
87 | }
88 |
89 | func testDeletionLeadingToInsertionDeletionMoves() {
90 | let o = [0, 1, 2, 3, 4, 5, 6, 7, 8]
91 | let n = [0, 2, 3, 4, 7, 6, 9, 5, 10]
92 | let result = diff(o, n)
93 | XCTAssertEqual([.delete(8), .delete(7), .delete(5), .delete(1), .insert(4), .insert(6), .insert(7), .insert(8)], result)
94 | }
95 |
96 | func testMovingWithEqualityChanges() {
97 | let o = [
98 | FakeItem(value: 0, eValue: 0),
99 | FakeItem(value: 1, eValue: 1),
100 | FakeItem(value: 2, eValue: 2)
101 | ]
102 | let n = [
103 | FakeItem(value: 2, eValue: 3),
104 | FakeItem(value: 1, eValue: 1),
105 | FakeItem(value: 0, eValue: 0)
106 | ]
107 | let result = diff(o, n)
108 | XCTAssertEqual([.delete(2), .delete(0), .insert(0), .insert(2), .update(0)], result)
109 | }
110 |
111 | func testDeletingEqualObjects() {
112 | let o = [0, 0, 0, 0]
113 | let n = [0, 0]
114 | let result = diff(o, n)
115 | XCTAssertEqual(2, result.count)
116 | }
117 |
118 | func testInsertingEqualObjects() {
119 | let o = [0, 0]
120 | let n = [0, 0, 0, 0]
121 | let result = diff(o, n)
122 | XCTAssertEqual(2, result.count)
123 | }
124 |
125 | func testInsertingWithOldArrayHavingMultipleCopies() {
126 | let o = [NSObject(), NSObject(), NSObject(), 49, 33, "cat", "cat", 0, 14] as [AnyHashable]
127 | var n = o
128 | n.insert("cat", at: 5)
129 | let result = diff(o, n)
130 | XCTAssertEqual(1, result.count)
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/Source/Core/Animation/Bezier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bezier.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/22/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Based on:
12 | // https://github.com/WebKit/webkit/blob/master/Source/WebCore/platform/graphics/UnitBezier.h
13 |
14 | /*
15 | * Copyright (C) 2008 Apple Inc. All Rights Reserved.
16 | *
17 | * Redistribution and use in source and binary forms, with or without
18 | * modification, are permitted provided that the following conditions
19 | * are met:
20 | * 1. Redistributions of source code must retain the above copyright
21 | * notice, this list of conditions and the following disclaimer.
22 | * 2. Redistributions in binary form must reproduce the above copyright
23 | * notice, this list of conditions and the following disclaimer in the
24 | * documentation and/or other materials provided with the distribution.
25 | *
26 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
27 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
29 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
30 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
31 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
32 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES LOSS OF USE, DATA, OR
33 | * PROFITS OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
34 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 | */
38 |
39 | struct UnitBezier {
40 | let cx: Double
41 | let bx: Double
42 | let ax: Double
43 | let cy: Double
44 | let by: Double
45 | let ay: Double
46 |
47 | init(p1x: Double, p1y: Double, p2x: Double, p2y: Double) {
48 | // Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1).
49 | cx = 3.0 * p1x
50 | bx = 3.0 * (p2x - p1x) - cx
51 | ax = 1.0 - cx - bx
52 |
53 | cy = 3.0 * p1y
54 | by = 3.0 * (p2y - p1y) - cy
55 | ay = 1.0 - cy - by
56 | }
57 |
58 | func solve(_ x: Double, epsilon: Double) -> Double {
59 | return sampleCurveY(solveCurveX(x, epsilon: epsilon))
60 | }
61 |
62 | private func sampleCurveX(_ t: Double) -> Double {
63 | // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
64 | return ((ax * t + bx) * t + cx) * t
65 | }
66 |
67 | private func sampleCurveY(_ t: Double) -> Double {
68 | return ((ay * t + by) * t + cy) * t
69 | }
70 |
71 | private func sampleCurveDerivativeX(_ t: Double) -> Double {
72 | return (3.0 * ax * t + 2.0 * bx) * t + cx
73 | }
74 |
75 | // Given an x value, find a parametric value it came from.
76 | private func solveCurveX(_ x: Double, epsilon: Double) -> Double {
77 | var t0: Double
78 | var t1: Double
79 | var t2 = x
80 | var x2: Double
81 | var d2: Double
82 |
83 | // First try a few iterations of Newton's method -- normally very fast.
84 | for _ in 0..<8 {
85 | x2 = sampleCurveX(t2) - x
86 | if fabs(x2) < epsilon {
87 | return t2
88 | }
89 | d2 = sampleCurveDerivativeX(t2)
90 | if fabs(d2) < 1e-6 {
91 | break
92 | }
93 | t2 = t2 - x2 / d2
94 | }
95 |
96 | // Fall back to the bisection method for reliability.
97 | t0 = 0.0
98 | t1 = 1.0
99 | t2 = x
100 |
101 | if t2 < t0 {
102 | return t0
103 | }
104 | if t2 > t1 {
105 | return t1
106 | }
107 |
108 | while t0 < t1 {
109 | x2 = sampleCurveX(t2)
110 | if fabs(x2 - x) < epsilon {
111 | return t2
112 | }
113 | if x > x2 {
114 | t0 = t2
115 | } else {
116 | t1 = t2
117 | }
118 | t2 = (t1 - t0) * 0.5 + t0
119 | }
120 |
121 | // Failure.
122 | return t2
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Source/Core/Animation/BezierInterpolator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BezierInterpolator.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/22/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension CAMediaTimingFunction {
12 | @nonobjc public static let linear = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
13 | @nonobjc public static let easeIn = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
14 | @nonobjc public static let easeOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
15 | @nonobjc public static let easeInEaseOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
16 |
17 | // See http://easings.net.
18 | @nonobjc public static let easeInQuad = CAMediaTimingFunction(controlPoints: 0.55, 0.085, 0.68, 0.53)
19 | @nonobjc public static let easeOutQuad = CAMediaTimingFunction(controlPoints: 0.25, 0.46, 0.45, 0.94)
20 | @nonobjc public static let easeInOutQuad = CAMediaTimingFunction(controlPoints: 0.455, 0.03, 0.515, 0.955)
21 |
22 | @nonobjc public static let easeInCubic = CAMediaTimingFunction(controlPoints: 0.55, 0.055, 0.675, 0.19)
23 | @nonobjc public static let easeOutCubic = CAMediaTimingFunction(controlPoints: 0.215, 0.61, 0.355, 1)
24 | @nonobjc public static let easeInOutCubic = CAMediaTimingFunction(controlPoints: 0.645, 0.045, 0.355, 1)
25 |
26 | @nonobjc public static let easeInQuart = CAMediaTimingFunction(controlPoints: 0.895, 0.03, 0.685, 0.22)
27 | @nonobjc public static let easeOutQuart = CAMediaTimingFunction(controlPoints: 0.165, 0.84, 0.44, 1)
28 | @nonobjc public static let easeInOutQuart = CAMediaTimingFunction(controlPoints: 0.77, 0, 0.175, 1)
29 |
30 | @nonobjc public static let easeInQuint = CAMediaTimingFunction(controlPoints: 0.755, 0.05, 0.855, 0.06)
31 | @nonobjc public static let easeOutQuint = CAMediaTimingFunction(controlPoints: 0.23, 1, 0.32, 1)
32 | @nonobjc public static let easeInOutQuint = CAMediaTimingFunction(controlPoints: 0.86, 0, 0.07, 1)
33 |
34 | @nonobjc public static let easeInExpo = CAMediaTimingFunction(controlPoints: 0.95, 0.05, 0.795, 0.035)
35 | @nonobjc public static let easeOutExpo = CAMediaTimingFunction(controlPoints: 0.19, 1, 0.22, 1)
36 | @nonobjc public static let easeInOutExpo = CAMediaTimingFunction(controlPoints: 1, 0, 0, 1)
37 |
38 | @nonobjc public static let easeInCirc = CAMediaTimingFunction(controlPoints: 0.6, 0.04, 0.98, 0.335)
39 | @nonobjc public static let easeOutCirc = CAMediaTimingFunction(controlPoints: 0.075, 0.82, 0.165, 1)
40 | @nonobjc public static let easeInOutCirc = CAMediaTimingFunction(controlPoints: 0.785, 0.135, 0.15, 0.86)
41 |
42 | @nonobjc public static let easeInBack = CAMediaTimingFunction(controlPoints: 0.6, -0.28, 0.735, 0.045)
43 | @nonobjc public static let easeOutBack = CAMediaTimingFunction(controlPoints: 0.175, 0.885, 0.32, 1.275)
44 | @nonobjc public static let easeInOutBack = CAMediaTimingFunction(controlPoints: 0.68, -0.55, 0.265, 1.55)
45 | }
46 |
47 | public class BezierInterpolator: Interpolator {
48 | private lazy var timingControlPoints: [Double] = [0, 0, 0, 0]
49 |
50 | public init(_ timingFunction: CAMediaTimingFunction = .linear) {
51 | configure(with: timingFunction)
52 | }
53 |
54 | public func interpolate(_ fromValue: T, _ toValue: T, _ elapsed: TimeInterval, _ duration: TimeInterval) -> T {
55 | let progress = elapsed / duration
56 | let bezier = UnitBezier(p1x: timingControlPoints[0], p1y: timingControlPoints[1], p2x: timingControlPoints[2], p2y: timingControlPoints[3])
57 | let epsilon = 1 / (1000 * duration)
58 | let p = bezier.solve(progress, epsilon: epsilon)
59 |
60 | return T.interpolate(fromValue, toValue, p)
61 | }
62 |
63 | private func configure(with timingFunction: CAMediaTimingFunction) {
64 | var points = Array(repeating: 0, count: 4)
65 | timingFunction.getControlPoint(at: 1, values: &points[0])
66 | timingFunction.getControlPoint(at: 2, values: &points[2])
67 | for (index, _) in points.enumerated() {
68 | timingControlPoints[index] = Double(points[index])
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/TemplateKit.xcodeproj/xcshareddata/xcschemes/TemplateKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/Source/iOS/UIKitRenderer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIKitRenderer.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum ElementType: ElementRepresentable {
12 | case box
13 | case button
14 | case text
15 | case textField
16 | case image
17 | case table
18 | case collection
19 | case activityIndicator
20 | case view(UIView)
21 | case component(ComponentCreation.Type)
22 |
23 | public var tagName: String {
24 | switch self {
25 | case .box:
26 | return "box"
27 | case .text:
28 | return "text"
29 | case .textField:
30 | return "textfield"
31 | case .image:
32 | return "image"
33 | case .button:
34 | return "button"
35 | case .table:
36 | return "table"
37 | case .collection:
38 | return "collection"
39 | case .activityIndicator:
40 | return "activityindicator"
41 | case .component(let ComponentType):
42 | return "\(ComponentType)"
43 | default:
44 | fatalError("Unknown element type")
45 | }
46 | }
47 |
48 | public func make(_ element: Element, _ owner: Node?, _ context: Context?) -> Node {
49 | switch (self, element) {
50 | case (.box, let element as ElementData):
51 | return NativeNode(element: element, children: element.children?.map { $0.build(withOwner: owner, context: nil) }, owner: owner, context: context)
52 | case (.text, let element as ElementData):
53 | return NativeNode(element: element, owner: owner, context: context)
54 | case (.textField, let element as ElementData):
55 | return NativeNode(element: element, owner: owner, context: context)
56 | case (.image, let element as ElementData):
57 | return NativeNode(element: element, owner: owner, context: context)
58 | case (.button, _):
59 | return Button(element: element, children: nil, owner: owner, context: context)
60 | case (.table, let element as ElementData):
61 | return Table(element: element, children: nil, owner: owner, context: context)
62 | case (.collection, let element as ElementData):
63 | return Collection(element: element, children: nil, owner: owner, context: context)
64 | case (.activityIndicator, let element as ElementData):
65 | return NativeNode(element: element, owner: owner, context: context)
66 | case (.view(let view), let element as ElementData):
67 | return ViewNode(view: view, element: element, owner: owner, context: context)
68 | case (.component(let ComponentType), _):
69 | return ComponentType.init(element: element, children: nil, owner: owner, context: context)
70 | default:
71 | fatalError("Supplied element \(element) does not match type \(self)")
72 | }
73 | }
74 |
75 | public func equals(_ other: ElementRepresentable) -> Bool {
76 | guard let otherType = other as? ElementType else {
77 | return false
78 | }
79 | return self == otherType
80 | }
81 | }
82 |
83 | public func ==(lhs: ElementType, rhs: ElementType) -> Bool {
84 | switch (lhs, rhs) {
85 | case (.box, .box), (.button, .button), (.text, .text), (.image, .image), (.textField, .textField), (.table, .table), (.activityIndicator, .activityIndicator), (.collection, .collection):
86 | return true
87 | case (.view(let lhsView), .view(let rhsView)):
88 | return lhsView === rhsView
89 | case (.component(let lhsClass), .component(let rhsClass)):
90 | return lhsClass == rhsClass
91 | default:
92 | return false
93 | }
94 | }
95 |
96 | class DefaultContext: Context {
97 | let templateService: TemplateService = XMLTemplateService()
98 | let updateQueue: DispatchQueue = DispatchQueue(label: "UIKitRenderer")
99 | }
100 |
101 | public class UIKitRenderer: Renderer {
102 | public typealias ViewType = UIView
103 |
104 | public static let defaultContext: Context = DefaultContext()
105 | }
106 |
--------------------------------------------------------------------------------
/Examples/Twitter/Source/OrderedDictionary.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OrderedDictionary.swift
3 | // TwitterClientExample
4 | //
5 | // Created by Matias Cudich on 10/27/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // TODO(mcudich): Convert this to an enum so we can use indirect recursive boxing.
12 | class OrderedDictionaryEntry {
13 | typealias EntryType = OrderedDictionaryEntry
14 | var key: Key?
15 | var value: Value?
16 |
17 | var previous: EntryType?
18 | var next: EntryType?
19 |
20 | init(key: Key?, value: Value?) {
21 | self.key = key
22 | self.value = value
23 | }
24 | }
25 |
26 | public struct OrderedDictionary: ExpressibleByDictionaryLiteral {
27 | fileprivate var items = Dictionary>()
28 | fileprivate var head: OrderedDictionaryEntry?
29 |
30 | public init() {}
31 |
32 | public init(dictionaryLiteral elements: (Key, Value)...) {
33 | for (key, value) in elements {
34 | self[key] = value
35 | }
36 | }
37 |
38 | public subscript (key: Key) -> Value? {
39 | get {
40 | return items[key]?.value
41 | }
42 | set (newValue) {
43 | if let newValue = newValue {
44 | _ = updateValue(newValue, forKey: key)
45 | } else {
46 | _ = removeValue(forKey: key)
47 | }
48 | }
49 | }
50 |
51 | public var first: (Key, Value)? {
52 | if let key = head?.key, let value = head?.value {
53 | return (key, value)
54 | }
55 | return nil
56 | }
57 |
58 | public var last: (Key, Value)? {
59 | if head?.previous == nil {
60 | return first
61 | }
62 | if let key = head?.previous?.key, let value = head?.previous?.value {
63 | return (key, value)
64 | }
65 | return nil
66 | }
67 |
68 | public var count: Int {
69 | return items.count
70 | }
71 |
72 | public var keys: LazyMapCollection, Key> {
73 | return self.lazy.map { (key, value) in
74 | return key
75 | }
76 | }
77 |
78 | public var values: LazyMapCollection, Value> {
79 | return self.lazy.map { (key, value) in
80 | return value
81 | }
82 | }
83 |
84 | public mutating func updateValue(_ value: Value, forKey key: Key) -> Value? {
85 | if let existingValue = items[key] {
86 | existingValue.value = value
87 | return value
88 | } else {
89 | let previous = head?.previous ?? head
90 | let newValue = OrderedDictionaryEntry(key: key, value: value)
91 |
92 | newValue.previous = previous
93 | previous?.next = newValue
94 |
95 | head?.previous = newValue
96 | if head == nil {
97 | head = newValue
98 | }
99 | return items.updateValue(newValue, forKey: key)?.value
100 | }
101 | }
102 |
103 | public mutating func removeValue(forKey key: Key) -> Value? {
104 | if let existingValue = items[key] {
105 | let previous = existingValue.previous
106 | let next = existingValue.next
107 | previous?.next = next
108 | next?.previous = previous
109 | if existingValue.key == head?.key {
110 | head = nil
111 | }
112 | return items.removeValue(forKey: key)?.value
113 | }
114 | return nil
115 | }
116 | }
117 |
118 | extension OrderedDictionary: Collection {
119 | public var startIndex: Int {
120 | return 0
121 | }
122 |
123 | public var endIndex: Int {
124 | return count
125 | }
126 |
127 | public func index(after i: Int) -> Int {
128 | return i + 1
129 | }
130 |
131 | public subscript(idx: Int) -> (Key, Value) {
132 | var count = 0
133 | var item = head
134 | while count < idx {
135 | item = item?.next
136 | count += 1
137 | }
138 | return (item!.key!, item!.value!)
139 | }
140 | }
141 |
142 | extension OrderedDictionary: Sequence {
143 | public func generate() -> AnyIterator<(Key, Value)> {
144 | var currentValue = head
145 |
146 | return AnyIterator() {
147 | let returnValue = currentValue
148 | currentValue = currentValue?.next
149 |
150 | if let value = returnValue, let k = value.key, let v = value.value {
151 | return (k, v)
152 | }
153 | return nil
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/Source/iOS/Button.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Button.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/20/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct ButtonProperties: Properties, EnableableProperties, ActivatableProperties {
12 | public var core = CoreProperties()
13 | public var textStyle = TextStyleProperties()
14 |
15 | public var text: String?
16 | public var backgroundImage: UIImage?
17 | public var image: UIImage?
18 | public var selected: Bool?
19 | public var enabled: Bool?
20 | public var active: Bool?
21 |
22 | public init() {}
23 |
24 | public init(_ properties: [String : Any]) {
25 | core = CoreProperties(properties)
26 | textStyle = TextStyleProperties(properties)
27 |
28 | text = properties.cast("text")
29 | selected = properties.cast("selected")
30 | enabled = properties.cast("enabled")
31 | active = properties.cast("active")
32 | }
33 |
34 | public mutating func merge(_ other: ButtonProperties) {
35 | core.merge(other.core)
36 | textStyle.merge(other.textStyle)
37 |
38 | merge(&text, other.text)
39 | merge(&selected, other.selected)
40 | merge(&enabled, other.enabled)
41 | merge(&active, other.active)
42 | }
43 |
44 | public func has(key: String, withValue value: String) -> Bool {
45 | switch key {
46 | case "selected":
47 | return selected == Bool.fromString(value)
48 | default:
49 | fatalError("This attribute is not yet supported")
50 | }
51 | }
52 | }
53 |
54 | public func ==(lhs: ButtonProperties, rhs: ButtonProperties) -> Bool {
55 | return lhs.textStyle == rhs.textStyle && lhs.text == rhs.text && lhs.backgroundImage == rhs.backgroundImage && lhs.image == rhs.image && lhs.selected == rhs.selected && lhs.enabled == rhs.enabled && lhs.active == rhs.active && lhs.equals(otherProperties: rhs)
56 | }
57 |
58 | public struct ButtonState: State {
59 | var active = false
60 |
61 | public init() {}
62 | }
63 |
64 | public func ==(lhs: ButtonState, rhs: ButtonState) -> Bool {
65 | return lhs.active == rhs.active
66 | }
67 |
68 | public class Button: Component {
69 | public override func willReceiveProperties(nextProperties: ButtonProperties) {
70 | updateState { state in
71 | state.active = nextProperties.active ?? false
72 | }
73 | }
74 |
75 | public override func render() -> Template {
76 | var properties = DefaultProperties()
77 | properties.core.layout = self.properties.core.layout
78 | properties.core.style = self.properties.core.style
79 | if self.properties.core.gestures.onTap != nil {
80 | properties.core.gestures.onTap = #selector(Button.handleTap)
81 | }
82 | if self.properties.core.gestures.onDoubleTap != nil {
83 | properties.core.gestures.onDoubleTap = #selector(Button.handleDoubleTap)
84 | }
85 | properties.core.gestures.onPress = #selector(Button.handlePress)
86 | properties.textStyle = self.properties.textStyle
87 |
88 | var childElements = [Element]()
89 | if let _ = self.properties.image {
90 | childElements.append(renderImage())
91 | }
92 | if let _ = self.properties.text {
93 | childElements.append(renderTitle())
94 | }
95 |
96 | return Template(box(properties, childElements))
97 | }
98 |
99 | private func renderImage() -> Element {
100 | var properties = ImageProperties()
101 | properties.image = self.properties.image
102 |
103 | return image(properties)
104 | }
105 |
106 | private func renderTitle() -> Element {
107 | var properties = TextProperties()
108 | properties.text = self.properties.text
109 |
110 | return text(properties)
111 | }
112 |
113 | @objc private func handleTap() {
114 | updateState(stateMutation: { $0.active = false }) { [weak self] in
115 | guard self?.properties.enabled ?? true else {
116 | return
117 | }
118 | self?.performSelector(self?.properties.core.gestures.onTap, with: self)
119 | }
120 | }
121 |
122 | @objc private func handlePress() {
123 | updateState { state in
124 | state.active = true
125 | }
126 | }
127 |
128 | @objc private func handleDoubleTap() {
129 | updateState(stateMutation: { $0.active = false }) { [weak self] in
130 | guard self?.properties.enabled ?? true else {
131 | return
132 | }
133 | self?.performSelector(self?.properties.core.gestures.onDoubleTap)
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Source/iOS/Text.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Text.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/3/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class TextLayout {
12 | var properties: TextProperties
13 |
14 | fileprivate lazy var layoutManager: NSLayoutManager = {
15 | let layoutManager = NSLayoutManager()
16 | layoutManager.addTextContainer(self.textContainer)
17 | layoutManager.usesFontLeading = false
18 | return layoutManager
19 | }()
20 |
21 | fileprivate lazy var textStorage: NSTextStorage = {
22 | let textStorage = NSTextStorage()
23 | textStorage.addLayoutManager(self.layoutManager)
24 | return textStorage
25 | }()
26 |
27 | fileprivate lazy var textContainer: NSTextContainer = {
28 | let textContainer = NSTextContainer()
29 | textContainer.lineFragmentPadding = 0
30 | return textContainer
31 | }()
32 |
33 | init(properties: TextProperties) {
34 | self.properties = properties
35 | }
36 |
37 | func sizeThatFits(_ size: CGSize) -> CGSize {
38 | applyProperties()
39 | textContainer.size = size;
40 | layoutManager.ensureLayout(for: textContainer)
41 |
42 | let measuredSize = layoutManager.usedRect(for: textContainer).size
43 |
44 | return CGSize(width: ceil(measuredSize.width), height: ceil(measuredSize.height))
45 | }
46 |
47 | fileprivate func drawText(in rect: CGRect) {
48 | applyProperties()
49 | textContainer.size = rect.size
50 | let glyphRange = layoutManager.glyphRange(for: textContainer);
51 |
52 | layoutManager.drawBackground(forGlyphRange: glyphRange, at: CGPoint.zero);
53 | layoutManager.drawGlyphs(forGlyphRange: glyphRange, at: CGPoint.zero);
54 | }
55 |
56 | private func applyProperties() {
57 | let fontName = properties.textStyle.fontName ?? UIFont.systemFont(ofSize: UIFont.systemFontSize).fontName
58 | let fontSize = properties.textStyle.fontSize ?? UIFont.systemFontSize
59 |
60 | guard let fontValue = UIFont(name: fontName, size: fontSize) else {
61 | fatalError("Attempting to use unknown font")
62 | }
63 |
64 | let paragraphStyle = NSMutableParagraphStyle()
65 | paragraphStyle.alignment = properties.textStyle.textAlignment ?? .natural
66 |
67 | let attributes: [String : Any] = [
68 | NSFontAttributeName: fontValue,
69 | NSForegroundColorAttributeName: properties.textStyle.color ?? .black,
70 | NSParagraphStyleAttributeName: paragraphStyle
71 | ]
72 |
73 | textStorage.setAttributedString(NSAttributedString(string: properties.text ?? "", attributes: attributes))
74 |
75 | textContainer.lineBreakMode = properties.textStyle.lineBreakMode ?? .byTruncatingTail
76 | }
77 | }
78 |
79 | public struct TextProperties: Properties {
80 | public var core = CoreProperties()
81 |
82 | public var textStyle = TextStyleProperties()
83 | public var text: String?
84 |
85 | public init() {}
86 |
87 | public init(_ properties: [String : Any]) {
88 | core = CoreProperties(properties)
89 | textStyle = TextStyleProperties(properties)
90 | text = properties.cast("text")
91 | }
92 |
93 | public mutating func merge(_ other: TextProperties) {
94 | core.merge(other.core)
95 | textStyle.merge(other.textStyle)
96 | merge(&text, other.text)
97 | }
98 | }
99 |
100 | public func ==(lhs: TextProperties, rhs: TextProperties) -> Bool {
101 | return lhs.text == rhs.text && lhs.textStyle == rhs.textStyle && lhs.equals(otherProperties: rhs)
102 | }
103 |
104 | public class Text: UILabel, NativeView {
105 | public weak var eventTarget: AnyObject?
106 | public lazy var eventRecognizers = EventRecognizers()
107 |
108 | public var properties = TextProperties() {
109 | didSet {
110 | applyCoreProperties()
111 | textLayout.properties = properties
112 | setNeedsDisplay()
113 | }
114 | }
115 |
116 | private lazy var textLayout: TextLayout = {
117 | return TextLayout(properties: self.properties)
118 | }()
119 |
120 | public required init() {
121 | super.init(frame: CGRect.zero)
122 |
123 | isUserInteractionEnabled = true
124 |
125 | applyCoreProperties()
126 | }
127 |
128 | public required init?(coder aDecoder: NSCoder) {
129 | fatalError("init(coder:) has not been implemented")
130 | }
131 |
132 | public override func drawText(in rect: CGRect) {
133 | textLayout.drawText(in: rect)
134 | }
135 |
136 | public override func touchesBegan(_ touches: Set, with event: UIEvent?) {
137 | super.touchesBegan(touches, with: event)
138 |
139 | touchesBegan()
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/Source/iOS/TextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextField.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/9/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct TextFieldProperties: Properties, FocusableProperties, EnableableProperties {
12 | public var core = CoreProperties()
13 |
14 | public var textStyle = TextStyleProperties()
15 | public var text: String?
16 | public var onChange: Selector?
17 | public var onSubmit: Selector?
18 | public var onBlur: Selector?
19 | public var onFocus: Selector?
20 | public var placeholder: String?
21 | public var enabled: Bool?
22 | public var focused: Bool?
23 |
24 | public init() {}
25 |
26 | public init(_ properties: [String : Any]) {
27 | core = CoreProperties(properties)
28 | textStyle = TextStyleProperties(properties)
29 |
30 | text = properties.cast("text")
31 | onChange = properties.cast("onChange")
32 | onSubmit = properties.cast("onSubmit")
33 | onBlur = properties.cast("onBlur")
34 | onFocus = properties.cast("onFocus")
35 | placeholder = properties.cast("placeholder")
36 | enabled = properties.cast("enabled")
37 | focused = properties.cast("focused")
38 | }
39 |
40 | public mutating func merge(_ other: TextFieldProperties) {
41 | core.merge(other.core)
42 | textStyle.merge(other.textStyle)
43 |
44 | merge(&text, other.text)
45 | merge(&onChange, other.onChange)
46 | merge(&onSubmit, other.onSubmit)
47 | merge(&onBlur, other.onBlur)
48 | merge(&onFocus, other.onFocus)
49 | merge(&placeholder, other.placeholder)
50 | merge(&enabled, other.enabled)
51 | merge(&focused, other.focused)
52 | }
53 | }
54 |
55 | public func ==(lhs: TextFieldProperties, rhs: TextFieldProperties) -> Bool {
56 | return lhs.text == rhs.text && lhs.textStyle == rhs.textStyle && lhs.onChange == rhs.onChange && lhs.onSubmit == rhs.onSubmit && lhs.onBlur == rhs.onBlur && lhs.onFocus == rhs.onFocus && lhs.placeholder == rhs.placeholder && lhs.enabled == rhs.enabled && lhs.focused == rhs.focused && lhs.equals(otherProperties: rhs)
57 | }
58 |
59 | public class TextField: UITextField, NativeView {
60 | public weak var eventTarget: AnyObject?
61 | public lazy var eventRecognizers = EventRecognizers()
62 |
63 | public var properties = TextFieldProperties() {
64 | didSet {
65 | applyProperties()
66 | }
67 | }
68 |
69 | private var lastSelectedRange: UITextRange?
70 |
71 | public required init() {
72 | super.init(frame: CGRect.zero)
73 |
74 | addTarget(self, action: #selector(TextField.onChange), for: .editingChanged)
75 | addTarget(self, action: #selector(TextField.onSubmit), for: .editingDidEndOnExit)
76 | }
77 |
78 | public required init?(coder aDecoder: NSCoder) {
79 | fatalError("init(coder:) has not been implemented")
80 | }
81 |
82 | func applyProperties() {
83 | applyCoreProperties()
84 | applyTextFieldProperties()
85 | }
86 |
87 | func applyTextFieldProperties() {
88 | let fontName = properties.textStyle.fontName ?? UIFont.systemFont(ofSize: UIFont.systemFontSize).fontName
89 | let fontSize = properties.textStyle.fontSize ?? UIFont.systemFontSize
90 | guard let fontValue = UIFont(name: fontName, size: fontSize) else {
91 | fatalError("Attempting to use unknown font")
92 | }
93 | let attributes: [String : Any] = [
94 | NSFontAttributeName: fontValue,
95 | NSForegroundColorAttributeName: properties.textStyle.color ?? .black
96 | ]
97 |
98 | selectedTextRange = lastSelectedRange
99 | tintColor = .black
100 |
101 | attributedText = NSAttributedString(string: properties.text ?? "", attributes: attributes)
102 | textAlignment = properties.textStyle.textAlignment ?? .natural
103 | placeholder = properties.placeholder
104 | isEnabled = properties.enabled ?? true
105 | if properties.focused ?? false {
106 | let _ = becomeFirstResponder()
107 | }
108 | }
109 |
110 | func onChange() {
111 | lastSelectedRange = selectedTextRange
112 | if let onChange = properties.onChange {
113 | let _ = eventTarget?.perform(onChange, with: self)
114 | }
115 | }
116 |
117 | func onSubmit() {
118 | if let onSubmit = properties.onSubmit {
119 | let _ = eventTarget?.perform(onSubmit, with: self)
120 | }
121 | }
122 |
123 | public override func becomeFirstResponder() -> Bool {
124 | if let onFocus = properties.onFocus {
125 | let _ = eventTarget?.perform(onFocus, with: self)
126 | }
127 | return super.becomeFirstResponder()
128 | }
129 |
130 | public override func resignFirstResponder() -> Bool {
131 | if let onBlur = properties.onBlur {
132 | let _ = eventTarget?.perform(onBlur, with: self)
133 | }
134 | return super.resignFirstResponder()
135 | }
136 |
137 | public override func touchesBegan(_ touches: Set, with event: UIEvent?) {
138 | super.touchesBegan(touches, with: event)
139 |
140 | touchesBegan()
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/Tests/NodeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeTests.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/24/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import TemplateKit
11 |
12 | extension NativeNode: Equatable {}
13 | public func ==(lhs: NativeNode, rhs: NativeNode) -> Bool {
14 | return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
15 | }
16 |
17 | class NodeTests: XCTestCase {
18 | func testBuild() {
19 | let child1 = box(DefaultProperties(["width": Float(50)]))
20 | let child2 = box(DefaultProperties(["width": Float(10)]))
21 | let child3 = image(ImageProperties(["width": Float(30)]))
22 |
23 | let tree = box(DefaultProperties(), [child1, child2, child3])
24 | let node = tree.build(withOwner: nil, context: nil) as? NativeNode
25 | let child1Node = node?.children?[0] as? NativeNode
26 | let child2Node = node?.children?[1] as? NativeNode
27 | let child3Node = node?.children?[2] as? NativeNode
28 |
29 | XCTAssertEqual(50, child1Node?.properties.core.layout.width)
30 | XCTAssertEqual(10, child2Node?.properties.core.layout.width)
31 | XCTAssertEqual(30, child3Node?.properties.core.layout.width)
32 |
33 | XCTAssertNotNil(child1Node?.parent)
34 | XCTAssertEqual(3, node?.children?.count)
35 | }
36 |
37 | func testUpdateChildProperties() {
38 | let child1 = box(DefaultProperties(["width": Float(50)]))
39 | let child2 = box(DefaultProperties(["width": Float(10)]))
40 | let child3 = image(ImageProperties(["width": Float(30)]))
41 |
42 | let tree = box(DefaultProperties(), [child1, child2, child3])
43 | let node = tree.build(withOwner: nil, context: nil) as? NativeNode
44 |
45 | let newChild1 = box(DefaultProperties(["width": Float(150)]))
46 | let newTree = box(DefaultProperties(), [newChild1, child2, child3])
47 | let child1Node = node?.children?[0] as? NativeNode
48 |
49 | node?.update(with: newTree)
50 | XCTAssertEqual(150, child1Node?.properties.core.layout.width)
51 | XCTAssertEqual(node?.children?[0] as? NativeNode, child1Node)
52 | }
53 |
54 | func testReplaceChild() {
55 | let child1 = box(DefaultProperties(["width": Float(50)]))
56 | let child2 = box(DefaultProperties(["width": Float(10)]))
57 | let child3 = image(ImageProperties(["width": Float(30)]))
58 |
59 | let tree = box(DefaultProperties(), [child1, child2, child3])
60 | let node = tree.build(withOwner: nil, context: nil) as? NativeNode
61 | let newChild1 = text(TextProperties())
62 | let newTree = box(DefaultProperties(), [newChild1, child2, child3])
63 |
64 | node?.update(with: newTree)
65 | let newTextNode = node?.children?[0] as? NativeNode
66 | XCTAssertNotNil(newTextNode)
67 | XCTAssertEqual(3, node?.children?.count)
68 | }
69 |
70 | func testMoveChild() {
71 | let child1 = box(DefaultProperties(["width": Float(1), "key": "1"]))
72 | let child2 = box(DefaultProperties(["width": Float(2), "key": "2"]))
73 | let child3 = box(DefaultProperties(["width": Float(3), "key": "3"]))
74 |
75 | let tree = box(DefaultProperties(), [child1, child2, child3])
76 | let node = tree.build(withOwner: nil, context: nil) as? NativeNode
77 | let builtChild1 = node?.children?[0] as? NativeNode
78 | let builtChild2 = node?.children?[1] as? NativeNode
79 | let builtChild3 = node?.children?[2] as? NativeNode
80 |
81 | let reorderedTree = box(DefaultProperties(), [child2, child3, child1])
82 | node?.update(with: reorderedTree)
83 |
84 | XCTAssertEqual(node?.children?[0] as? NativeNode, builtChild2)
85 | XCTAssertEqual(node?.children?[1] as? NativeNode, builtChild3)
86 | XCTAssertEqual(node?.children?[2] as? NativeNode, builtChild1)
87 | }
88 |
89 | func testAddChildren() {
90 | let child1 = box(DefaultProperties(["width": Float(1), "key": "1"]))
91 | let child2 = box(DefaultProperties(["width": Float(2), "key": "2"]))
92 | let child3 = box(DefaultProperties(["width": Float(3), "key": "3"]))
93 |
94 | let tree = box(DefaultProperties(), [child1])
95 | let node = tree.build(withOwner: nil, context: nil) as? NativeNode
96 | let builtChild1 = node?.children?[0] as? NativeNode
97 |
98 | let smallerTree = box(DefaultProperties(), [child1, child2, child3])
99 |
100 | XCTAssertEqual(1, node?.children?.count)
101 | node?.update(with: smallerTree)
102 | XCTAssertEqual(3, node?.children?.count)
103 | XCTAssertEqual(node?.children?[0] as? NativeNode, builtChild1)
104 | }
105 |
106 | func testRemoveChildren() {
107 | let child1 = box(DefaultProperties(["width": Float(1), "key": "1"]))
108 | let child2 = box(DefaultProperties(["width": Float(2), "key": "2"]))
109 | let child3 = box(DefaultProperties(["width": Float(3), "key": "3"]))
110 |
111 | let tree = box(DefaultProperties(), [child1, child2, child3])
112 | let node = tree.build(withOwner: nil, context: nil) as? NativeNode
113 | let smallerTree = box(DefaultProperties(), [child1])
114 |
115 | XCTAssertEqual(3, node?.children?.count)
116 | node?.update(with: smallerTree)
117 | XCTAssertEqual(1, node?.children?.count)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Source/Core/Node.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import CSSLayout
3 |
4 | public protocol Node: class, Keyable {
5 | weak var owner: Node? { get set }
6 | weak var parent: Node? { get set }
7 | var context: Context? { get set }
8 | var view: View { get }
9 |
10 | var children: [Node]? { get set }
11 | var cssNode: CSSNode? { get set }
12 | var type: ElementRepresentable { get }
13 |
14 | func build() -> View
15 | func update(with newElement: Element)
16 | func forceUpdate()
17 | func computeLayout(availableWidth: Float, availableHeight: Float) -> CSSLayout
18 | func buildCSSNode() -> CSSNode
19 | func updateCSSNode()
20 | func getContext() -> Context
21 | }
22 |
23 | public extension Node {
24 | func computeLayout(availableWidth: Float = .nan, availableHeight: Float = .nan) -> CSSLayout {
25 | return buildCSSNode().layout(availableWidth: availableWidth, availableHeight: availableHeight)
26 | }
27 |
28 | func shouldReplace(type: ElementRepresentable, with otherType: ElementRepresentable) -> Bool {
29 | return !type.equals(otherType)
30 | }
31 |
32 | func updateParent() {
33 | for child in (children ?? []) {
34 | child.parent = self
35 | }
36 | }
37 |
38 | func getContext() -> Context {
39 | guard let context = context ?? owner?.getContext() else {
40 | fatalError("No context available")
41 | }
42 | return context
43 | }
44 | }
45 |
46 | public protocol PropertyNode: Node {
47 | associatedtype PropertiesType: Properties
48 |
49 | var element: ElementData { get set }
50 | var properties: PropertiesType { get set }
51 |
52 | func shouldUpdate(nextProperties: PropertiesType) -> Bool
53 | func performDiff()
54 | }
55 |
56 | public extension PropertyNode {
57 | public var key: String? {
58 | get {
59 | return properties.core.identifier.key
60 | }
61 | set {
62 | properties.core.identifier.key = newValue
63 | }
64 | }
65 |
66 | var type: ElementRepresentable {
67 | return element.type
68 | }
69 |
70 | func update(with newElement: Element) {
71 | let newElement = newElement as! ElementData
72 | let nextProperties = newElement.properties
73 | let shouldUpdate = self.shouldUpdate(nextProperties: nextProperties)
74 | performUpdate(with: newElement, nextProperties: nextProperties, shouldUpdate: shouldUpdate)
75 | }
76 |
77 | func forceUpdate() {
78 | performUpdate(with: element, nextProperties: properties, shouldUpdate: true)
79 | }
80 |
81 | func performUpdate(with newElement: ElementData, nextProperties: PropertiesType, shouldUpdate: Bool) {
82 | element = newElement
83 |
84 | if properties != nextProperties {
85 | properties = nextProperties
86 | updateCSSNode()
87 | }
88 |
89 | if shouldUpdate {
90 | performDiff()
91 | }
92 | }
93 |
94 | // Nodes by default perform child diffs, regardless of whether they themselves updated. This
95 | // function can be overriden by nodes that want finer-grained control over whether their subtree
96 | // is evaluated.
97 | func performDiff() {
98 | diffChildren(newChildren: element.children ?? [])
99 | }
100 |
101 | // Nodes should always update by default.
102 | func shouldUpdate(nextProperties: PropertiesType) -> Bool {
103 | return true
104 | }
105 |
106 | private func diffChildren(newChildren: [Element]) {
107 | var currentChildren = (children ?? []).keyed { index, elm in
108 | computeKey(index, elm)
109 | }
110 |
111 | for (index, element) in newChildren.enumerated() {
112 | let key = computeKey(index, element)
113 | guard let currentChild = currentChildren[key] else {
114 | append(element)
115 | continue
116 | }
117 | currentChildren.removeValue(forKey: key)
118 |
119 | if shouldReplace(type: currentChild.type, with: element.type) {
120 | replace(currentChild, with: element)
121 | } else {
122 | move(child: currentChild, to: index)
123 | currentChild.update(with: element)
124 | }
125 | }
126 |
127 | for (_, child) in currentChildren {
128 | remove(child: child)
129 | }
130 | }
131 |
132 | private func computeKey(_ index: Int, _ keyable: Keyable) -> String {
133 | return keyable.key ?? "\(index)"
134 | }
135 |
136 | private func insert(child: Node, at index: Int) {
137 | children?.insert(child, at: index)
138 | cssNode?.insertChild(child: child.buildCSSNode(), at: index)
139 | }
140 |
141 | private func remove(child: Node) {
142 | guard let index = index(of: child) else {
143 | return
144 | }
145 | children?.remove(at: index)
146 | if let childNode = child.cssNode {
147 | cssNode?.removeChild(child: childNode)
148 | }
149 | }
150 |
151 | private func move(child: Node, to index: Int) {
152 | guard let currentIndex = self.index(of: child), currentIndex != index else {
153 | return
154 | }
155 | children?.remove(at: currentIndex)
156 | if let childNode = child.cssNode {
157 | cssNode?.removeChild(child: childNode)
158 | }
159 | insert(child: child, at: index)
160 | }
161 |
162 | private func index(of child: Node) -> Int? {
163 | return children?.index(where: { $0 === child })
164 | }
165 |
166 | private func replace(_ node: Node, with element: Element) {
167 | let replacement = element.build(withOwner: owner, context: nil)
168 | let index = self.index(of: node)!
169 | remove(child: node)
170 | insert(child: replacement, at: index)
171 | }
172 |
173 | private func append(_ element: Element) {
174 | let child = element.build(withOwner: owner, context: nil)
175 | children?.insert(child, at: children!.endIndex)
176 | cssNode?.insertChild(child: child.buildCSSNode(), at: children!.count - 1)
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/Source/Utilities/Diff.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Diff.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/30/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum DiffStep: Equatable {
12 | case insert(Int)
13 | case delete(Int)
14 | case move(Int, Int)
15 | case update(Int)
16 | }
17 |
18 | public func ==(lhs: DiffStep, rhs: DiffStep) -> Bool {
19 | switch (lhs, rhs) {
20 | case let (.insert(lhsIndex), .insert(rhsIndex)):
21 | return lhsIndex == rhsIndex
22 | case let (.delete(lhsIndex), .delete(rhsIndex)):
23 | return lhsIndex == rhsIndex
24 | case let (.move(lhsFromIndex, lhsToIndex), .move(rhsFromIndex, rhsToIndex)):
25 | return lhsFromIndex == rhsFromIndex && lhsToIndex == rhsToIndex
26 | case let (.update(lhsIndex), .update(rhsIndex)):
27 | return lhsIndex == rhsIndex
28 | default:
29 | return false
30 | }
31 | }
32 |
33 | enum Counter {
34 | case zero
35 | case one
36 | case many
37 |
38 | mutating func increment() {
39 | switch self {
40 | case .zero:
41 | self = .one
42 | case .one:
43 | self = .many
44 | case .many:
45 | break
46 | }
47 | }
48 | }
49 |
50 | class SymbolEntry {
51 | var oc: Counter = .zero
52 | var nc: Counter = .zero
53 | var olno = [Int]()
54 |
55 | var occursInBoth: Bool {
56 | return oc != .zero && nc != .zero
57 | }
58 | }
59 |
60 | enum Entry {
61 | case symbol(SymbolEntry)
62 | case index(Int)
63 | }
64 |
65 | // Based on http://dl.acm.org/citation.cfm?id=359467.
66 | //
67 | // And other implementations at:
68 | // * https://github.com/Instagram/IGListKit
69 | // * https://github.com/andre-alves/PHDiff
70 | //
71 | public func diff(_ old: [T], _ new: [T]) -> [DiffStep] {
72 | var table = [Int: SymbolEntry]()
73 | var oa = [Entry]()
74 | var na = [Entry]()
75 |
76 | // Pass 1 comprises the following: (a) each line i of file N is read in sequence; (b) a symbol
77 | // table entry for each line i is created if it does not already exist; (c) NC for the line's
78 | // symbol table entry is incremented; and (d) NA [i] is set to point to the symbol table entry of
79 | // line i.
80 | for item in new {
81 | let entry = table[item.hashValue] ?? SymbolEntry()
82 | table[item.hashValue] = entry
83 | entry.nc.increment()
84 | na.append(.symbol(entry))
85 | }
86 |
87 | // Pass 2 is identical to pass 1 except that it acts on file O, array OA, and counter OC,
88 | // and OLNO for the symbol table entry is set to the line's number.
89 | for (index, item) in old.enumerated() {
90 | let entry = table[item.hashValue] ?? SymbolEntry()
91 | table[item.hashValue] = entry
92 | entry.oc.increment()
93 | entry.olno.append(index)
94 | oa.append(.symbol(entry))
95 | }
96 |
97 | // In pass 3 we use observation 1 and process only those lines having NC = OC = 1. Since each
98 | // represents (we assume) the same unmodified line, for each we replace the symbol table pointers
99 | // in NA and OA by the number of the line in the other file. For example, if NA[i] corresponds to
100 | // such a line, we look NA[i] up in the symbol table and set NA[i] to OLNO and OA[OLNO] to i.
101 | // In pass 3 we also "find" unique virtual lines immediately before the first and immediately
102 | // after the last lines of the files.
103 | for (index, item) in na.enumerated() {
104 | if case .symbol(let entry) = item, entry.occursInBoth {
105 | guard entry.olno.count > 0 else { continue }
106 |
107 | let oldIndex = entry.olno.removeFirst()
108 | na[index] = .index(oldIndex)
109 | oa[oldIndex] = .index(index)
110 | }
111 | }
112 |
113 | // In pass 4, we apply observation 2 and process each line in NA in ascending order: If NA[i]
114 | // points to OA[j] and NA[i + 1] and OA[j + 1] contain identical symbol table entry pointers, then
115 | // OA[j + 1] is set to line i + 1 and NA[i + 1] is set to line j + 1.
116 | var i = 1
117 | while (i < na.count - 1) {
118 | if case .index(let j) = na[i], j + 1 < oa.count {
119 | if case .symbol(let newEntry) = na[i + 1], case .symbol(let oldEntry) = oa[j + 1], newEntry === oldEntry {
120 | na[i + 1] = .index(j + 1)
121 | oa[j + 1] = .index(i + 1)
122 | }
123 | }
124 | i += 1
125 | }
126 |
127 | // In pass 5, we also apply observation 2 and process each entry in descending order: if NA[i]
128 | // points to OA[j] and NA[i - 1] and OA[j - 1] contain identical symbol table pointers, then
129 | // NA[i - 1] is replaced by j - 1 and OA[j - 1] is replaced by i - 1.
130 | i = na.count - 1
131 | while (i > 0) {
132 | if case .index(let j) = na[i], j - 1 >= 0 {
133 | if case .symbol(let newEntry) = na[i - 1], case .symbol(let oldEntry) = oa[j - 1], newEntry === oldEntry {
134 | na[i - 1] = .index(j - 1)
135 | oa[j - 1] = .index(i - 1)
136 | }
137 | }
138 | i -= 1
139 | }
140 |
141 | var steps = [DiffStep]()
142 |
143 | var deleteOffsets = Array(repeating: 0, count: old.count)
144 | var runningOffset = 0
145 | for (index, item) in oa.enumerated() {
146 | deleteOffsets[index] = runningOffset
147 | if case .symbol(_) = item {
148 | steps.append(.delete(index))
149 | runningOffset += 1
150 | }
151 | }
152 |
153 | runningOffset = 0
154 |
155 | for (index, item) in na.enumerated() {
156 | switch item {
157 | case .symbol(_):
158 | steps.append(.insert(index))
159 | runningOffset += 1
160 | case .index(let oldIndex):
161 | // The object has changed, so it should be updated.
162 | if old[oldIndex] != new[index] {
163 | steps.append(.update(index))
164 | }
165 |
166 | let deleteOffset = deleteOffsets[oldIndex]
167 | // The object is not at the expected position, so move it.
168 | if (oldIndex - deleteOffset + runningOffset) != index {
169 | steps.append(.move(oldIndex, index))
170 | }
171 | }
172 | }
173 |
174 | // Sort the steps to allow for a single-pass array update.
175 | var insertions = [DiffStep]()
176 | var updates = [DiffStep]()
177 | var indexedDeletions: [[DiffStep]] = Array(repeating: [], count: old.count)
178 |
179 | for step in steps {
180 | switch step {
181 | case .insert:
182 | insertions.append(step)
183 | case let .delete(fromIndex):
184 | indexedDeletions[fromIndex].append(step)
185 | case let .move(fromIndex, toIndex):
186 | insertions.append(.insert(toIndex))
187 | indexedDeletions[fromIndex].append(.delete(fromIndex))
188 | case .update:
189 | updates.append(step)
190 | }
191 | }
192 |
193 | let deletions = indexedDeletions.flatMap { $0.first }.reversed()
194 |
195 | return deletions + insertions + updates
196 | }
197 |
--------------------------------------------------------------------------------
/Source/Template/XMLTemplateService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XMLTemplateService.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/11/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CSSParser
11 |
12 | class XMLTemplateParser: Parser {
13 | typealias ParsedType = XMLDocument
14 |
15 | required init() {}
16 |
17 | func parse(data: Data) throws -> XMLDocument {
18 | return try XMLDocument(data: data)
19 | }
20 | }
21 |
22 | class StyleSheetParser: Parser {
23 | typealias ParsedType = String
24 |
25 | required init() {}
26 |
27 | func parse(data: Data) -> String {
28 | return String(data: data, encoding: .utf8) ?? ""
29 | }
30 | }
31 |
32 | public class XMLTemplateService: TemplateService {
33 | public var cachePolicy: CachePolicy {
34 | set {
35 | templateResourceService.cachePolicy = newValue
36 | }
37 | get {
38 | return templateResourceService.cachePolicy
39 | }
40 | }
41 |
42 | public var liveReloadInterval = DispatchTimeInterval.seconds(5)
43 |
44 | private lazy var requestQueue: DispatchQueue = DispatchQueue(label: "XMLTemplateService")
45 | private lazy var templateResourceService: ResourceService = {
46 | return ResourceService(requestQueue: self.requestQueue)
47 | }()
48 | private lazy var styleSheetResourceService: ResourceService = {
49 | return ResourceService(requestQueue: self.requestQueue)
50 | }()
51 |
52 | private let liveReload: Bool
53 | public lazy var templates = [URL: Template]()
54 | private lazy var observers = [URL: NSHashTable]()
55 |
56 | public init(liveReload: Bool = false) {
57 | self.liveReload = liveReload
58 | }
59 |
60 | public func fetchTemplates(withURLs urls: [URL], completion: @escaping (Result) -> Void) {
61 | if urls.isEmpty {
62 | return completion(.success())
63 | }
64 |
65 | if cachePolicy == .never {
66 | URLCache.shared.removeAllCachedResponses()
67 | }
68 | var pendingURLs = Set(urls)
69 | for url in urls {
70 | templateResourceService.load(url) { [weak self] result in
71 | switch result {
72 | case .success(let templateXML):
73 | guard let componentElement = templateXML.componentElement else {
74 | completion(.failure(TemplateKitError.parserError("No component element found in template at \(url)")))
75 | return
76 | }
77 |
78 | self?.resolveStyles(for: templateXML, at: url) { styleSheet in
79 | self?.templates[url] = Template(componentElement, styleSheet ?? StyleSheet())
80 | pendingURLs.remove(url)
81 | if pendingURLs.isEmpty {
82 | completion(.success())
83 | if self?.liveReload ?? false {
84 | self?.watchTemplates(withURLs: urls)
85 | }
86 | }
87 | }
88 | case .failure(_):
89 | pendingURLs.remove(url)
90 | completion(.failure(TemplateKitError.missingTemplate("Template not found at \(url)")))
91 | }
92 | }
93 | }
94 | }
95 |
96 | public func addObserver(observer: Node, forLocation location: URL) {
97 | if !liveReload {
98 | return
99 | }
100 | let observers = self.observers[location] ?? NSHashTable.weakObjects()
101 |
102 | observers.add(observer as AnyObject)
103 | self.observers[location] = observers
104 | }
105 |
106 | public func removeObserver(observer: Node, forLocation location: URL) {
107 | observers[location]?.remove(observer as AnyObject)
108 | }
109 |
110 | private func resolveStyles(for template: XMLDocument, at relativeURL: URL, completion: @escaping (StyleSheet?) -> Void) {
111 | var urls = [URL]()
112 | var sheets = [String](repeating: "", count: template.styleElements.count)
113 | for (index, styleElement) in template.styleElements.enumerated() {
114 | if let urlString = styleElement.attributes["url"], let url = URL(string: urlString, relativeTo: relativeURL) {
115 | urls.append(url)
116 | } else {
117 | sheets[index] = styleElement.value ?? ""
118 | }
119 | }
120 |
121 | let done = { (fetchedSheets: [String]) in
122 | completion(StyleSheet(string: fetchedSheets.joined(), inheritedProperties: ["fontName", "fontSize", "color", "textAlignment"]))
123 | }
124 |
125 | var pendingURLs = Set(urls)
126 | if pendingURLs.isEmpty {
127 | return done(sheets)
128 | }
129 |
130 | for (index, url) in urls.enumerated() {
131 | styleSheetResourceService.load(url) { result in
132 | pendingURLs.remove(url)
133 | switch result {
134 | case .success(let sheetString):
135 | sheets[index] = sheetString
136 | if pendingURLs.isEmpty {
137 | done(sheets)
138 | }
139 | case .failure(_):
140 | done(sheets)
141 | }
142 | }
143 | }
144 | }
145 |
146 | private func watchTemplates(withURLs urls: [URL]) {
147 | let time = DispatchTime.now() + liveReloadInterval
148 | DispatchQueue.main.asyncAfter(deadline: time) {
149 | let cachedCopies = self.templates
150 | self.fetchTemplates(withURLs: urls) { [weak self] result in
151 | for url in urls {
152 | if self?.templates[url] != cachedCopies[url], let observers = self?.observers[url] {
153 | for observer in observers.allObjects {
154 | (observer as! Node).forceUpdate()
155 | }
156 | }
157 | }
158 | }
159 | }
160 | }
161 | }
162 |
163 | extension XMLDocument {
164 | var hasRemoteStyles: Bool {
165 | return styleElements.contains { element in
166 | return element.attributes["url"] != nil
167 | }
168 | }
169 |
170 | var styleElements: [XMLElement] {
171 | return root?.children.filter { candidate in
172 | return candidate.name == "style"
173 | } ?? []
174 | }
175 |
176 | var componentElement: XMLElement? {
177 | return root?.children.first { candidate in
178 | return candidate.name != "style"
179 | }
180 | }
181 | }
182 |
183 | extension XMLElement: ElementProvider {
184 | func build(with model: Model) -> Element {
185 | guard let built = maybeBuild(with: model) else {
186 | fatalError("Templates may not be empty")
187 | }
188 | return built
189 | }
190 |
191 | func equals(_ other: ElementProvider?) -> Bool {
192 | guard let other = other as? XMLElement else {
193 | return false
194 | }
195 | return self == other
196 | }
197 |
198 | private func maybeBuild(with model: Model) -> Element? {
199 | let resolvedProperties = model.resolve(properties: attributes)
200 | if let ifDirective: Bool = resolvedProperties.cast("if"), !ifDirective {
201 | return nil
202 | }
203 | return NodeRegistry.shared.buildElement(with: name, properties: resolvedProperties, children: children.flatMap { $0.maybeBuild(with: model) })
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/Source/iOS/AsyncDataListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AsyncDataListView.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 9/12/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol AsyncDataListView: class {
12 | var operationQueue: AsyncQueue { get }
13 | var context: Context { get set }
14 | weak var eventTarget: Node? { get set }
15 | var nodeCache: [[Node?]] { get set }
16 |
17 | func insertItems(at indexPaths: [IndexPath], completion: @escaping () -> Void)
18 | func deleteItems(at indexPaths: [IndexPath], completion: @escaping () -> Void)
19 | func insertSections(_ sections: IndexSet, completion: @escaping () -> Void)
20 | func deleteSections(_ sections: IndexSet, completion: @escaping () -> Void)
21 | func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, completion: @escaping () -> Void)
22 | func moveSection(_ section: Int, toSection newSection: Int, completion: @escaping () -> Void)
23 | func reloadItems(at indexPaths: [IndexPath], completion: @escaping () -> Void)
24 | func reloadSections(_ sections: IndexSet, completion: @escaping () -> Void)
25 | func reloadData(completion: @escaping () -> Void)
26 |
27 | func element(at indexPath: IndexPath) -> Element?
28 | func node(at indexPath: IndexPath) -> Node?
29 | func totalNumberOfSections() -> Int
30 | func totalNumberOfRows(in section: Int) -> Int?
31 | }
32 |
33 | extension AsyncDataListView {
34 | func insertItems(at indexPaths: [IndexPath], completion: @escaping () -> Void) {
35 | precacheNodes(at: indexPaths)
36 | operationQueue.enqueueOperation { done in
37 | DispatchQueue.main.async {
38 | completion()
39 | done()
40 | }
41 | }
42 | }
43 |
44 | func deleteItems(at indexPaths: [IndexPath], completion: @escaping () -> Void) {
45 | purgeNodes(at: indexPaths)
46 | operationQueue.enqueueOperation { done in
47 | DispatchQueue.main.async {
48 | completion()
49 | done()
50 | }
51 | }
52 | }
53 |
54 | func insertSections(_ sections: IndexSet, completion: @escaping () -> Void) {
55 | precacheNodes(in: sections)
56 | operationQueue.enqueueOperation { done in
57 | DispatchQueue.main.async {
58 | completion()
59 | done()
60 | }
61 | }
62 | }
63 |
64 | func deleteSections(_ sections: IndexSet, completion: @escaping () -> Void) {
65 | purgeNodes(in: sections)
66 | operationQueue.enqueueOperation { done in
67 | DispatchQueue.main.async {
68 | completion()
69 | done()
70 | }
71 | }
72 | }
73 |
74 | func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, completion: @escaping () -> Void) {
75 | let node = nodeCache[indexPath.section].remove(at: indexPath.row)
76 | nodeCache[newIndexPath.section].insert(node, at: newIndexPath.row)
77 |
78 | operationQueue.enqueueOperation { done in
79 | DispatchQueue.main.async {
80 | completion()
81 | done()
82 | }
83 | }
84 | }
85 |
86 | func moveSection(_ section: Int, toSection newSection: Int, completion: @escaping () -> Void) {
87 | let section = nodeCache.remove(at: section)
88 | nodeCache.insert(section, at: newSection)
89 |
90 | operationQueue.enqueueOperation { done in
91 | DispatchQueue.main.async {
92 | completion()
93 | done()
94 | }
95 | }
96 | }
97 |
98 | func reloadItems(at indexPaths: [IndexPath], completion: @escaping () -> Void) {
99 | precacheNodes(at: indexPaths)
100 | operationQueue.enqueueOperation { done in
101 | DispatchQueue.main.async {
102 | completion()
103 | done()
104 | }
105 | }
106 | }
107 |
108 | func reloadSections(_ sections: IndexSet, completion: @escaping () -> Void) {
109 | precacheNodes(in: sections)
110 | operationQueue.enqueueOperation { done in
111 | DispatchQueue.main.async {
112 | completion()
113 | done()
114 | }
115 | }
116 | }
117 |
118 | func reloadData(completion: @escaping () -> Void) {
119 | let sectionCount = totalNumberOfSections()
120 | let indexPaths: [IndexPath] = (0.. Node? {
135 | return nodeCache[indexPath.section][indexPath.row]
136 | }
137 |
138 | private func indexPaths(forSection section: Int) -> [IndexPath] {
139 | let expectedRowCount = totalNumberOfRows(in: section) ?? 0
140 | return (0.. Void) {
161 | if indexPaths.count == 0 {
162 | return done()
163 | }
164 |
165 | var pending = indexPaths.count
166 | for indexPath in indexPaths.sorted(by: { $0.row < $1.row }) {
167 | guard let element = self.element(at: indexPath) else {
168 | continue
169 | }
170 |
171 | UIKitRenderer.render(element, context: context as Context) { node in
172 | self.cacheNode(node, at: indexPath)
173 | pending -= 1
174 | if pending == 0 {
175 | done()
176 | }
177 | }
178 | }
179 | }
180 |
181 | private func cacheNode(_ node: Node, at indexPath: IndexPath) {
182 | if nodeCache.count <= indexPath.section {
183 | nodeCache.append([Node?]())
184 | }
185 | node.owner = eventTarget
186 |
187 | var delta = indexPath.row - nodeCache[indexPath.section].count
188 | while delta >= 0 {
189 | nodeCache[indexPath.section].append(nil)
190 | delta -= 1
191 | }
192 | nodeCache[indexPath.section][indexPath.row] = node
193 | }
194 |
195 | private func purgeNodes(at indexPaths: [IndexPath]) {
196 | operationQueue.enqueueOperation { done in
197 | self.performPurge(for: indexPaths, done: done)
198 | }
199 | }
200 |
201 | private func purgeNodes(in sections: IndexSet) {
202 | operationQueue.enqueueOperation { done in
203 | let indexPaths: [IndexPath] = sections.reduce([]) { previous, section in
204 | return previous + self.indexPaths(forSection: section)
205 | }
206 | self.performPurge(for: indexPaths, done: done)
207 | }
208 | }
209 |
210 | private func performPurge(for indexPaths: [IndexPath], done: @escaping () -> Void) {
211 | if indexPaths.count == 0 {
212 | return done()
213 | }
214 |
215 | for indexPath in indexPaths {
216 | nodeCache[indexPath.section].remove(at: indexPath.row)
217 | }
218 |
219 | done()
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/Source/iOS/Table.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Table.swift
3 | // TemplateKit
4 | //
5 | // Created by Matias Cudich on 10/29/16.
6 | // Copyright © 2016 Matias Cudich. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CSSLayout
11 |
12 | public struct TableSection: Hashable {
13 | public let items: [AnyHashable]
14 | public var hashValue: Int
15 |
16 | public init(items: [AnyHashable], hashValue: Int) {
17 | self.items = items
18 | self.hashValue = hashValue
19 | }
20 | }
21 |
22 | public func ==(lhs: TableSection, rhs: TableSection) -> Bool {
23 | return false
24 | }
25 |
26 | public struct TableProperties: Properties {
27 | public var core = CoreProperties()
28 |
29 | weak public var tableViewDelegate: TableViewDelegate?
30 | weak public var tableViewDataSource: TableViewDataSource?
31 | weak public var eventTarget: Node?
32 |
33 | public var isEditing: Bool?
34 |
35 | // This is used to know when the underlying table view rows should be inserted, deleted or moved.
36 | // This 2-d array should follow the list of sections and rows provided by the data source. When
37 | // this value changes, the table is automatically updated for you using the minimal set of
38 | // operations required.
39 | public var items: [TableSection]?
40 |
41 | public var onEndReached: Selector?
42 | public var onEndReachedThreshold: CGFloat?
43 |
44 | public init() {}
45 |
46 | public init(_ properties: [String : Any]) {
47 | core = CoreProperties(properties)
48 |
49 | tableViewDelegate = properties["tableViewDelegate"] as? TableViewDelegate
50 | tableViewDataSource = properties["tableViewDataSource"] as? TableViewDataSource
51 | eventTarget = properties["eventTarget"] as? Node
52 | isEditing = properties.cast("isEditing")
53 | items = properties["items"] as? [TableSection]
54 | onEndReached = properties.cast("onEndReached")
55 | onEndReachedThreshold = properties.cast("onEndReachedThreshold")
56 | }
57 |
58 | public mutating func merge(_ other: TableProperties) {
59 | core.merge(other.core)
60 |
61 | merge(&tableViewDelegate, other.tableViewDelegate)
62 | merge(&tableViewDataSource, other.tableViewDataSource)
63 | merge(&eventTarget, other.eventTarget)
64 | merge(&isEditing, other.isEditing)
65 | merge(&items, other.items)
66 | merge(&onEndReached, other.onEndReached)
67 | merge(&onEndReachedThreshold, other.onEndReachedThreshold)
68 | }
69 | }
70 |
71 | public func ==(lhs: TableProperties, rhs: TableProperties) -> Bool {
72 | return lhs.tableViewDelegate === rhs.tableViewDelegate && lhs.tableViewDataSource === rhs.tableViewDataSource && lhs.eventTarget === rhs.eventTarget && lhs.isEditing == rhs.isEditing && lhs.items == rhs.items && lhs.onEndReached == rhs.onEndReached && lhs.onEndReachedThreshold == rhs.onEndReachedThreshold && lhs.equals(otherProperties: rhs)
73 | }
74 |
75 | public class Table: PropertyNode {
76 | public weak var parent: Node?
77 | public weak var owner: Node?
78 | public var context: Context?
79 |
80 | public var properties: TableProperties {
81 | didSet {
82 | if let oldItems = oldValue.items, let newItems = properties.items {
83 | precondition(newItems.count > 0, "Items must contain at least one section.")
84 | updateRows(old: oldItems, new: newItems)
85 | }
86 | }
87 | }
88 | public var children: [Node]?
89 | public var element: ElementData
90 | public var cssNode: CSSNode?
91 |
92 | lazy public var view: View = {
93 | return TableView(frame: CGRect.zero, style: .plain, context: self.getContext())
94 | }()
95 |
96 | private var tableView: TableView? {
97 | return view as? TableView
98 | }
99 |
100 | private lazy var scrollProxy: ScrollProxy = {
101 | let proxy = ScrollProxy()
102 | proxy.delegate = self
103 | return proxy
104 | }()
105 |
106 | private var delegateProxy: DelegateProxy?
107 | fileprivate var lastReportedEndLength: CGFloat = 0
108 |
109 | public required init(element: ElementData, children: [Node]? = nil, owner: Node? = nil, context: Context? = nil) {
110 | self.element = element
111 | self.properties = element.properties
112 | self.children = children
113 | self.owner = owner
114 | self.context = context
115 |
116 | if let items = properties.items {
117 | precondition(items.count > 0, "Items must contain at least one section.")
118 | }
119 |
120 | updateParent()
121 | }
122 |
123 | public func build() -> View {
124 | if delegateProxy == nil || delegateProxy?.target !== properties.tableViewDelegate {
125 | delegateProxy = DelegateProxy(target: properties.tableViewDelegate, interceptor: scrollProxy)
126 | delegateProxy?.registerInterceptable(selector: #selector(UIScrollViewDelegate.scrollViewDidScroll(_:)))
127 | }
128 |
129 | if tableView?.tableViewDelegate !== delegateProxy {
130 | tableView?.tableViewDelegate = delegateProxy as? TableViewDelegate
131 | }
132 | if tableView?.tableViewDataSource !== properties.tableViewDataSource {
133 | tableView?.tableViewDataSource = properties.tableViewDataSource
134 | }
135 |
136 | tableView?.eventTarget = properties.eventTarget
137 | if tableView?.isEditing != properties.isEditing {
138 | tableView?.setEditing(properties.isEditing ?? false, animated: true)
139 | }
140 |
141 | return view
142 | }
143 |
144 | private func updateRows(old: [TableSection], new: [TableSection]) {
145 | tableView?.beginUpdates()
146 |
147 | let sectionResult = diff(old, new)
148 | for step in sectionResult {
149 | switch step {
150 | case .delete(let index):
151 | tableView?.deleteSections(IndexSet(integer: index), with: .none)
152 | case .insert(let index):
153 | tableView?.insertSections(IndexSet(integer: index), with: .none)
154 | case .update(let index):
155 | // Updates are handled below.
156 | break
157 | default:
158 | break
159 | }
160 | }
161 |
162 | var deletions = [IndexPath]()
163 | var insertions = [IndexPath]()
164 | var updates = [IndexPath]()
165 |
166 | for (sectionIndex, section) in new.enumerated() {
167 | let oldItems = old.count > sectionIndex ? old[sectionIndex].items : []
168 | let result = diff(oldItems, section.items)
169 | for step in result {
170 | switch step {
171 | case .delete(let index):
172 | deletions.append(IndexPath(row: index, section: sectionIndex))
173 | case .insert(let index):
174 | insertions.append(IndexPath(row: index, section: sectionIndex))
175 | case .update(let index):
176 | updates.append(IndexPath(row: index, section: sectionIndex))
177 | default:
178 | break
179 | }
180 | }
181 | }
182 |
183 | tableView?.deleteRows(at: deletions, with: .none)
184 | tableView?.insertRows(at: insertions, with: .none)
185 | tableView?.reloadRows(at: updates, with: .none)
186 |
187 | tableView?.endUpdates()
188 | }
189 | }
190 |
191 | extension Table: ScrollProxyDelegate {
192 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
193 | properties.tableViewDelegate?.scrollViewDidScroll?(scrollView)
194 |
195 | let threshold = properties.onEndReachedThreshold ?? 0
196 | if let onEndReached = properties.onEndReached, scrollView.contentSize.height != lastReportedEndLength, distanceFromEnd(of: scrollView) < threshold, let owner = owner {
197 | lastReportedEndLength = scrollView.contentSize.height
198 | _ = (owner as AnyObject).perform(onEndReached)
199 | }
200 | }
201 |
202 | private func distanceFromEnd(of scrollView: UIScrollView) -> CGFloat {
203 | return scrollView.contentSize.height - (scrollView.bounds.height + scrollView.contentOffset.y)
204 | }
205 | }
206 |
--------------------------------------------------------------------------------