├── 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 | 12 | -------------------------------------------------------------------------------- /TemplateKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/Resources/SimpleTemplate.xml: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------