├── Example
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── ExampleCounter
│ ├── ExampleCounterViewModel.swift
│ ├── ExampleCounterState.swift
│ └── ExampleCounterVC.swift
├── Utilities
│ └── ExampleViewModel.swift
├── ExampleList
│ ├── ExampleListViewModel.swift
│ ├── ExampleListState.swift
│ └── CompositeVC.swift
├── ExampleLogin
│ ├── UsernameState+PasswordState.swift
│ └── UsernameViewModel+PasswordViewModel.swift
├── Info.plist
├── Base.lproj
│ └── LaunchScreen.storyboard
└── AppDelegate.swift
├── ComponentViewController-Class-Hierarchy.png
├── Cyanic.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ ├── IDETemplateMacros.plist
│ └── xcschemes
│ ├── Example.xcscheme
│ └── Cyanic.xcscheme
├── .sourcery.yml
├── Cyanic.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Sources
├── Extensions
│ ├── CGSize+Cyanic.swift
│ ├── Array+Cyanic.swift
│ └── Text+Cyanic.swift
├── Utilities
│ ├── Constants.swift
│ ├── MultiSectionController.swift
│ ├── ComponentsController.swift
│ └── SectionController.swift
├── Protocols
│ ├── CyanicError.swift
│ ├── Selectable.swift
│ ├── UserInterfaceModel.swift
│ ├── State.swift
│ ├── ComponentLayout.swift
│ ├── CyanicChildVCType.swift
│ ├── Copyable.swift
│ ├── StateObservableBuilder.swift
│ └── Component.swift
├── Enums
│ ├── CellSize.swift
│ ├── Size.swift
│ ├── ThrottleType.swift
│ ├── Async.swift
│ └── ComponentStateValidator.swift
├── Components
│ ├── Protocols
│ │ ├── StaticSpacingComponentType.swift
│ │ ├── SizedComponentType.swift
│ │ ├── ChildVCComponentType.swift
│ │ ├── StaticTextComponentType.swift
│ │ ├── StaticLabelComponentType.swift
│ │ ├── ButtonComponentType.swift
│ │ ├── ExpandableComponentType.swift
│ │ ├── TextFieldComponentType.swift
│ │ └── TextViewComponentType.swift
│ ├── ExpandableComponent
│ │ ├── DividerLine.swift
│ │ ├── ExpandableComponent.swift
│ │ └── ExpandableComponentLayout.swift
│ ├── StaticSpacingComponent
│ │ ├── StaticSpacingComponent.swift
│ │ └── StaticSpacingComponentLayout.swift
│ ├── ChildVCComponent
│ │ ├── ChildVCComponent.swift
│ │ └── ChildVCComponentLayout.swift
│ ├── SizedComponent
│ │ ├── SizedComponent.swift
│ │ └── SizedComponentLayout.swift
│ ├── StaticLabelComponent
│ │ ├── StaticLabelComponentLayout.swift
│ │ └── StaticLabelComponent.swift
│ ├── StaticTextComponent
│ │ ├── StaticTextComponent.swift
│ │ └── StaticTextComponentLayout.swift
│ ├── ButtonComponent
│ │ ├── ButtonComponent.swift
│ │ └── ButtonComponentLayout.swift
│ ├── TextFieldComponent
│ │ ├── CyanicTextFieldDelegateProxy.swift
│ │ ├── TextFieldComponent.swift
│ │ └── TextFieldComponentLayout.swift
│ └── TextViewComponent
│ │ ├── TextViewComponentLayout.swift
│ │ ├── TextViewComponent.swift
│ │ └── CyanicTextViewDelegateProxy.swift
├── Classes
│ ├── AnyViewModel.swift
│ ├── AnyComponent.swift
│ ├── AbstractViewModel.swift
│ └── CyanicViewController.swift
├── TableView
│ ├── TableComponentViewController.swift
│ ├── TableComponentCell.swift
│ └── TableComponentSectionView.swift
├── CollectionView
│ ├── CollectionComponentCell.swift
│ ├── CyanicNoFadeFlowLayout.swift
│ ├── CollectionComponentViewController.swift
│ └── ComponentSupplementaryView.swift
├── Views
│ └── ComponentView.swift
└── Sourcery
│ └── AutoHashableComponent+Cyanic.swift
├── Podfile
├── Supporting Files
├── FFUFComponents.h
└── Info.plist
├── Tests
├── Info.plist
├── AsyncTests.swift
├── CyanicViewControllerTests.swift
└── StateStoreTests.swift
├── LICENSE
├── Templates
├── CyanicComponent.xctemplate
│ ├── TemplateInfo.plist
│ └── ___FILEBASENAME___Component.swift
├── AutoGenerateComponent.stencil
├── AutoHashableComponent.stencil
├── AutoEquatableComponent.stencil
└── AutoGenerateComponentExtensions.swifttemplate
├── .swiftlint.yml
├── Cyanic.podspec
├── Podfile.lock
├── .gitignore
└── README.md
/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/ComponentViewController-Class-Hierarchy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feilfeilundfeil/Cyanic/HEAD/ComponentViewController-Class-Hierarchy.png
--------------------------------------------------------------------------------
/Cyanic.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.sourcery.yml:
--------------------------------------------------------------------------------
1 | sources:
2 | - Sources/Components
3 | - Sources/Protocols
4 | templates:
5 | - Templates
6 | output:
7 | Sources/Sourcery
8 | disableCache:
9 | true
10 | args:
11 | "project": "Cyanic"
12 | "isFramework": true
13 |
--------------------------------------------------------------------------------
/Cyanic.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Cyanic.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Cyanic.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/ExampleCounter/ExampleCounterViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleCounterViewModel.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 3/27/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Cyanic
10 |
11 | class ExampleCounterViewModel: ExampleViewModel {}
12 |
--------------------------------------------------------------------------------
/Sources/Extensions/CGSize+Cyanic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 05.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import CoreGraphics
8 |
9 | extension CGSize: Hashable {
10 |
11 | public func hash(into hasher: inout Hasher) {
12 | self.height.hash(into: &hasher)
13 | self.width.hash(into: &hasher)
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | platform :ios, '10.0'
2 |
3 | use_frameworks!
4 |
5 | def pods
6 | pod 'RxSwift'
7 | pod 'RxDataSources'
8 | pod 'LayoutKit', :git => 'https://github.com/hooliooo/LayoutKit.git'
9 | pod 'Sourcery'
10 | end
11 |
12 | target 'Cyanic' do
13 | pods
14 | end
15 |
16 | target 'Tests' do
17 | pod 'Quick'
18 | pod 'Nimble'
19 | end
20 |
21 | target 'Example' do
22 | pod 'SideMenu'
23 | pod 'Alacrity'
24 | pod 'Kio'
25 | end
26 |
--------------------------------------------------------------------------------
/Cyanic.xcodeproj/xcshareddata/IDETemplateMacros.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FILEHEADER
6 |
7 | // ___PACKAGENAME___
8 | // Created by ___FULLUSERNAME___ on ___DATE___.
9 | // Licensed under the MIT license. See LICENSE file
10 | //
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Supporting Files/FFUFComponents.h:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic.h
3 | // Cyanic
4 | //
5 | // Created by Julio Miguel Alorro on 2/6/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for Cyanic.
12 | FOUNDATION_EXPORT double CyanicVersionNumber;
13 |
14 | //! Project version string for Cyanic.
15 | FOUNDATION_EXPORT const unsigned char CyanicVersionString[];
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Example/Utilities/ExampleViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleViewModel.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 4/12/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Cyanic
10 |
11 | public class ExampleViewModel: ViewModel {
12 |
13 | public init(initialState: ConcreteState) {
14 | super.init(initialState: initialState, isDebugMode: true)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Example/ExampleList/ExampleListViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleViewModel.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 3/27/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Cyanic
10 |
11 | public final class ExampleListViewModel: ExampleViewModel {
12 |
13 | // MARK: Methods
14 | func buttonWasTapped() {
15 | self.setState { $0.hasTextInTextField = !$0.hasTextInTextField }
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Utilities/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 11.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import CoreGraphics
8 | import Foundation
9 | import UIKit
10 |
11 | public enum Constants {
12 | internal static var screenWidth: CGFloat { return UIScreen.main.bounds.width }
13 | internal static var bundleIdentifier: String { return Bundle.main.bundleIdentifier ?? "de.ffuf.Cyanic" }
14 | public static var invalidID: String = UUID().uuidString
15 | }
16 |
--------------------------------------------------------------------------------
/Example/ExampleCounter/ExampleCounterState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleCounterState.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 3/27/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Cyanic
10 |
11 | struct ExampleCounterState: State {
12 |
13 | // MARK: Static Properties
14 | static var `default`: ExampleCounterState {
15 | return ExampleCounterState(changeCount: 0)
16 | }
17 |
18 | // MARK: Stored Properties
19 | var changeCount: Int
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Protocols/CyanicError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.05.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import Foundation
8 |
9 | /**
10 | This protocol exists because using the Error protocol's localizedDescription is unreliable. Using localizedDescription
11 | to diff when overriding the default implmentation does not work as of 14.05.2019.
12 | */
13 | public protocol CyanicError: Error {
14 |
15 | /**
16 | String representation of the CyanicError instance
17 | */
18 | var errorDescription: String { get }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Protocols/Selectable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 05.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | /**
10 | Selectable is a protocol adopted by Components that want to utilize the collectionView(collectionView:didSelectItemAt:)
11 | method in SingleSectionComponentViewController.
12 | */
13 | public protocol Selectable {
14 |
15 | /**
16 | Code block executed when collectionView(collectionView:didSelectItemAt:) or tableView(tableView:, didSelectRowAt:)
17 | method is called.
18 | */
19 | func onSelect(_ view: UIView)
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Protocols/UserInterfaceModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 04.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | /**
8 | The UserInterfaceModel protocol is a workaround protocol to be able to access a Component's layout without casting
9 | it to Component (which causes a generic constraint error).
10 | */
11 | public protocol UserInterfaceModel {
12 |
13 | // sourcery: isLayout = true
14 | // sourcery: skipHashing,skipEquality,
15 | /// The LayoutKit related class that will calculate size, location and configuration of the subviews in the ComponentCell
16 | var layout: ComponentLayout { get }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Enums/CellSize.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 31.10.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import CoreGraphics
8 |
9 | /**
10 | CellSize is an enum that defines the sizing for a CollectionComponentViewController
11 | */
12 | public enum CellSize {
13 |
14 | /**
15 | Configures the UICollectionViewCell size to have the width of the UICollectionView and the height computed by the
16 | ComponentLayout that represents the cell.
17 | */
18 | case list
19 |
20 | /**
21 | Configures the UICollectionViewCell size to be the associated CGSize.
22 | */
23 | case exactly(() -> CGSize)
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Example/ExampleLogin/UsernameState+PasswordState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UsernameState+PasswordState.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 3/27/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Cyanic
10 |
11 | public struct UsernameState: State {
12 |
13 | public static var `default`: UsernameState {
14 | return UsernameState(userName: "")
15 | }
16 |
17 | public var userName: String
18 |
19 | }
20 |
21 | public struct PasswordState: State {
22 |
23 | public static var `default`: PasswordState {
24 | return PasswordState(password: "")
25 | }
26 |
27 | public var password: String
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Example/ExampleLogin/UsernameViewModel+PasswordViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UsernameViewModel+PasswordViewModel.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 3/27/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Cyanic
10 |
11 | public final class UsernameViewModel: ExampleViewModel {
12 |
13 | public func setUserName(_ userName: String) {
14 | self.setState(with: { $0.userName = userName })
15 | }
16 |
17 | }
18 |
19 | public final class PasswordViewModel: ExampleViewModel {
20 |
21 | public func setPassword(_ password: String) {
22 | self.setState(with: { $0.password = password })
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Components/Protocols/StaticSpacingComponentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 01.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | // sourcery: AutoEquatableComponent,AutoHashableComponent
10 | // sourcery: Component = StaticSpacingComponent,isFrameworkComponent
11 | /// StaticSpacingComponentType is a protocol for Components that represent space between
12 | /// other components / content on the screen.
13 | public protocol StaticSpacingComponentType: StaticHeightComponent {
14 |
15 | // sourcery: defaultValue = UIColor.clear
16 | /// The backgroundColor of the spacing.
17 | var backgroundColor: UIColor { get set }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Enums/Size.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import CoreGraphics
8 |
9 | /**
10 | In cases where ComponentViewController is a childViewController, it is sometimes necessary to have an exact size.
11 | This enum allows the the programmer to specify if there's an exact size for the ComponentViewController or if it
12 | should be taken cared of by UIKit.
13 | */
14 | public enum Size {
15 | /**
16 | Size is defined by UIKit automatically. It is calculated by taking the UICollectionView's frame.
17 | */
18 | case automatic
19 |
20 | /**
21 | Size is defined by a constant value.
22 | */
23 | case exactly(CGSize)
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Extensions/Array+Cyanic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 23.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import RxSwift
8 |
9 | internal extension Array where Element == AnyViewModel {
10 |
11 | /**
12 | Combines the ViewModels' state observables into one combinedLatest Observable<[Any]>
13 | - Returns:
14 | The ViewModels' states as an Observable<[Any]>
15 | */
16 | func combineStateObservables() -> Observable<[Any]> {
17 | let stateObservables: [Observable] = self.map { $0.state }
18 | let combinedStatesObservables: Observable<[Any]> = Observable.combineLatest(stateObservables)
19 | return combinedStatesObservables
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/Protocols/State.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | /**
8 | State is a protocol adopted structs representing the state of your view model.
9 | */
10 | public protocol State: Hashable, Copyable {
11 |
12 | /**
13 | The default State instance.
14 | */
15 | static var `default`: Self { get }
16 |
17 | }
18 |
19 | /**
20 | State type that has expandable UI and needs to persist the isExpanded/isCollapsed state.
21 | */
22 | public protocol ExpandableState: State {
23 |
24 | /**
25 | The dictionary that contains the isExpanded/isCollapsed state of ExpandableComponentTypes.
26 | */
27 | var expandableDict: [String: Bool] { get set }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
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 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Sources/Protocols/ComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 09.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import RxSwift
9 |
10 | /**
11 | A ComponentLayout is simply a Layout that is customized for Cyanic.
12 |
13 | A ComponentLayout calculates the size and location of the subviews in a given CGRect. The subviews are styled based on the
14 | data of the Component that owns this ComponentLayout
15 | */
16 | public protocol ComponentLayout: class, Layout {}
17 |
18 | public extension ComponentLayout {
19 |
20 | /**
21 | The String identifier of the ComponentLayout used for viewReuseId in LayoutKit
22 | */
23 | static var identifier: String {
24 | return String(describing: self)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Enums/ThrottleType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 10.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import RxSwift
8 |
9 | /**
10 | Represents the throttling option available for ComponentViewController and CyanicViewController
11 | */
12 | public enum ThrottleType {
13 |
14 | /**
15 | Changes in State are ignored until a specified time interval has passed, the time interval is reset
16 | when changes in State occur within the time interval.
17 | */
18 | case debounce(RxTimeInterval)
19 |
20 | /**
21 | Changes in State are ignored until a specified time interval has passed.
22 | */
23 | case throttle(RxTimeInterval)
24 |
25 | /**
26 | Changes in State are immediately emitted.
27 | */
28 | case none
29 | }
30 |
--------------------------------------------------------------------------------
/Supporting Files/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
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 | 0.4.5
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Sources/Protocols/CyanicChildVCType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 07.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | /**
10 | The protocol that UIViewControllers adopt in order to be shown inside a ComponentViewController via
11 | a ChildVCComponent. Call the **cleanUp** method inside `deinit`.
12 | */
13 | public protocol CyanicChildVCType {}
14 |
15 | public extension CyanicChildVCType where Self: UIViewController {
16 |
17 | func cleanUp() {
18 | self.view?.removeFromSuperview()
19 | self.willMove(toParent: nil)
20 | self.removeFromParent()
21 | }
22 |
23 | }
24 |
25 | /**
26 | The default CyanicChildVCType in a ChildVCComponent. This MUST be replaced by another custom CyanicChildVCType.
27 | Otherwise, it forces a fatalError.
28 | */
29 | internal final class InvalidChildComponentVC: UIViewController, CyanicChildVCType {}
30 |
--------------------------------------------------------------------------------
/Sources/Protocols/Copyable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 01.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | /**
8 | Copyable is a fork of https://gist.github.com/nicklockwood/9b4aac87e7f88c80e932ba3c843252df.
9 | Used to mutate Components and State in place with a copy (due to the value-type nature of structs)
10 | */
11 | public protocol Copyable {}
12 |
13 | public extension Copyable {
14 | /**
15 | Creates a mutable copy of Self and mutates that copy with the closure.
16 | - Parameters:
17 | - block: The closure that mutates the mutable copy of Self
18 | - mutableSelf: The mutable copy of Self passed to the closure.
19 | - Returns:
20 | A copy of Self with the changes from the closure.
21 | */
22 | func copy(with changes: (_ mutableSelf: inout Self) -> Void) -> Self {
23 | var mutableSelf: Self = self
24 | changes(&mutableSelf)
25 | return mutableSelf
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Feil, Feil, & Feil GmbH
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/Templates/CyanicComponent.xctemplate/TemplateInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Kind
6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind
7 | Platforms
8 |
9 | com.apple.platform.iphoneos
10 |
11 | Options
12 |
13 |
14 | Identifier
15 | productName
16 | Required
17 |
18 | Name
19 | Component Name:
20 | Description
21 | The name of the Component to be generated. E.g. 'MyCustom' results in 'MyCustomComponent'.
22 | Type
23 | text
24 | Default
25 | MyCustom
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Sources/Classes/AnyViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 23.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import RxSwift
8 |
9 | /**
10 | Type-erased wrapper for a ViewModel instance.
11 | */
12 | public final class AnyViewModel {
13 |
14 | /**
15 | Initializer.
16 | Keeps the underlying viewModel instance in memory as an Any type.
17 | - Parameters:
18 | - viewModel: The BaseViewModel instance to be type erased.
19 | */
20 | public init>(_ viewModel: ConcreteViewModel) {
21 | self.viewModel = viewModel
22 | self.state = viewModel.state.map({ (state: ConcreteState) -> Any in state as Any })
23 | self.isDebugMode = viewModel.isDebugMode
24 | }
25 |
26 | /**
27 | The underlying ViewModel as an Any type
28 | */
29 | public let viewModel: Any
30 |
31 | /**
32 | The ViewModel's State as an Observable
33 | */
34 | public let state: Observable
35 |
36 | /**
37 | The debug mode of the underlying ViewModel instance
38 | */
39 | public let isDebugMode: Bool
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Components/ExpandableComponent/DividerLine.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 10.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | /**
10 | DividerLine is a data structure representing the characteristics of the UIView used a "divider line" on the ExpandableComponent.
11 | */
12 | public struct DividerLine {
13 |
14 | /**
15 | Initializer.
16 | - Parameters:
17 | - backgroundColor: The UIColor of the divider line UIView.
18 | - insets: The insets of the divider line UIView.
19 | - height: The height of the divider line.
20 | */
21 | public init(backgroundColor: UIColor, insets: UIEdgeInsets, height: CGFloat) {
22 | self.backgroundColor = backgroundColor
23 | self.insets = insets
24 | self.height = height
25 | }
26 |
27 | /**
28 | The UIColor used by the divider line UIView
29 | */
30 | public let backgroundColor: UIColor
31 |
32 | /**
33 | The UIEdgeInsets used by the divider line UIView.
34 | */
35 | public let insets: UIEdgeInsets
36 |
37 | /**
38 | The height used by the divider line UIView
39 | */
40 | public let height: CGFloat
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | opt_in_rules:
2 | - attributes
3 | - duplicate_imports
4 | - discouraged_optional_boolean
5 | - discouraged_optional_collection
6 | - empty_count
7 | - empty_enum_arguments
8 | # - explicit_acl
9 | - explicit_init
10 | - explicit_self
11 | # - explicit_top_level_acl
12 | # - explicit_type_interface
13 | # - file_name
14 | - first_where
15 | - force_unwrapping
16 | - identical_operands
17 | - identifier_name
18 | - implicitly_unwrapped_optional
19 | - last_where
20 | - literal_expression_end_indentation
21 | - lower_acl_than_parent
22 | - multiline_arguments
23 | - multiline_function_chains
24 | - multiline_literal_brackets
25 | - multiple_closures_with_trailing_closure
26 | - pattern_matching_keywords
27 | - unused_import
28 | - unused_private_declaration
29 | - vertical_parameter_alignment
30 | - vertical_parameter_alignment_on_call
31 | - yoda_condition
32 |
33 | disabled_rules:
34 | - identifier_name
35 | - switch_case_alignment
36 |
37 | excluded: # paths to ignore during linting. Takes precedence over `included`.
38 | - Pods
39 | - Carthage
40 | - Tests
41 | - Example
42 | - Templates
43 |
44 | line_length:
45 | ignores_function_declarations: true
46 | ignores_comments: true
47 | warning: 130
48 |
49 | file_length:
50 | warning: 390
51 | error: 400
52 |
--------------------------------------------------------------------------------
/Sources/Components/StaticSpacingComponent/StaticSpacingComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension
10 | // sourcery: ComponentLayout = StaticSpacingComponentLayout
11 | /// StaticSpacingComponent is a Component that represents static spacing between content / other Components.
12 | public struct StaticSpacingComponent: StaticSpacingComponentType {
13 |
14 | // sourcery:inline:auto:StaticSpacingComponent.AutoGenerateComponent
15 | /**
16 | Work around Initializer because memberwise initializers are all or nothing
17 | - Parameters:
18 | - id: The unique identifier of the StaticSpacingComponent.
19 | */
20 | public init(id: String) {
21 | self.id = id
22 | }
23 |
24 | public var id: String
25 |
26 | public var width: CGFloat = 0.0
27 |
28 | public var height: CGFloat = 44.0
29 |
30 | public var backgroundColor: UIColor = UIColor.clear
31 |
32 | // sourcery: skipHashing, skipEquality
33 | public var layout: ComponentLayout { return StaticSpacingComponentLayout(component: self) }
34 |
35 | public var identity: StaticSpacingComponent { return self }
36 | // sourcery:end
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Components/StaticSpacingComponent/StaticSpacingComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | /**
11 | The StaticSpacingComponentLayout is a ComponentLayout that is a subclass of SizeLayout.
12 | Used to create, size, and arrange the subviews associated with StaticSpacingComponent.
13 | */
14 | public final class StaticSpacingComponentLayout: SizeLayout, ComponentLayout {
15 |
16 | /**
17 | Initializer.
18 | - Parameters:
19 | - component: The StaticSpacingComponent whose properties define the UI characters of the subviews to be created.
20 | */
21 | public init(component: StaticSpacingComponent) {
22 | let size: CGSize = component.size
23 | super.init(
24 | minWidth: size.width,
25 | maxWidth: size.width,
26 | minHeight: size.height,
27 | maxHeight: size.height,
28 | alignment: Alignment.center,
29 | flexibility: Flexibility.inflexible,
30 | viewReuseId: StaticSpacingComponentLayout.identifier,
31 | sublayout: nil,
32 | config: { (view: UIView) -> Void in
33 | view.backgroundColor = component.backgroundColor
34 | }
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Extensions/Text+Cyanic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import Foundation
8 | import LayoutKit
9 |
10 | extension Text: Hashable {
11 |
12 | /**
13 | The associated string value of either the String from .unattributed or the NSAttributedString.string from
14 | .attributed cases.
15 | */
16 | public var value: String {
17 | switch self {
18 | case .unattributed(let string):
19 | return string
20 | case .attributed(let string):
21 | return string.string
22 | }
23 | }
24 |
25 | public static func == (lhs: Text, rhs: Text) -> Bool {
26 | switch (lhs, rhs) {
27 | case let (.attributed(lhsString), attributed(rhsString)):
28 | return lhsString.isEqual(to: rhsString)
29 | case let (.unattributed(lhsString), .unattributed(rhsString)):
30 | return lhsString == rhsString
31 | default:
32 | return false
33 | }
34 | }
35 |
36 | public func hash(into hasher: inout Hasher) {
37 | switch self {
38 | case .attributed(let string):
39 | string.hash(into: &hasher)
40 | case .unattributed(let string):
41 | string.hash(into: &hasher)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Tests/AsyncTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AsyncTests.swift
3 | // Tests
4 | //
5 | // Created by Julio Miguel Alorro on 5/14/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | @testable import Cyanic
12 |
13 | class AsyncTests: QuickSpec {
14 |
15 | enum Error: CyanicError {
16 |
17 | case invalidThis(String)
18 | case invalidThat(String)
19 |
20 | var errorDescription: String {
21 | switch self {
22 | case .invalidThat(let value):
23 | return "That" + value
24 | case .invalidThis(let value):
25 | return "This" + value
26 | }
27 | }
28 |
29 | }
30 |
31 | override func spec() {
32 | describe("Async failures") {
33 | it("Should equal when values are equal") {
34 | let failOne: Async = .failure(AsyncTests.Error.invalidThis("This"))
35 | let failTwo: Async = .failure(AsyncTests.Error.invalidThis("This"))
36 |
37 | expect(failOne).to(equal(failTwo))
38 | }
39 |
40 | it("Should not equal when values are different") {
41 | let failOne: Async = .failure(AsyncTests.Error.invalidThis("This"))
42 | let failTwo: Async = .failure(AsyncTests.Error.invalidThis("That"))
43 |
44 | expect(failOne).toNot(equal(failTwo))
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
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 | UIRequiredDeviceCapabilities
26 |
27 | armv7
28 |
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Templates/AutoGenerateComponent.stencil:
--------------------------------------------------------------------------------
1 | {% for type in types.all|annotated:"AutoGenerateComponent" %}
2 | // sourcery:inline:auto:{{ type.name }}.AutoGenerateComponent
3 | /**
4 | Work around Initializer because memberwise initializers are all or nothing
5 | - Parameters:
6 | - id: The unique identifier of the {{ type.name }}.
7 | */
8 | {{ type.accessLevel }} init(id: String) {
9 | self.id = id
10 | }
11 |
12 | {% for var in type.allVariables|!annotated:"isExcluded" %}
13 | {% if var.annotations.skipHashing and var.annotations.skipEquality %}
14 | // sourcery: skipHashing, skipEquality
15 | {% elif var.annotations.skipHashing %}
16 | // sourcery: skipHashing
17 | {% elif var.annotations.skipEquality %}
18 | // sourcery: skipEquality
19 | {% endif %}
20 | {% if var.annotations.defaultValue %}
21 | {{ type.accessLevel }} {% if var.annotations.isWeak %}weak {% endif %}{% if var.annotations.isLazy %}lazy {% endif %}{% if var.isMutable %}var{% else %}let{% endif %} {{ var.name }}: {{ var.typeName }} = {{ var.annotations.defaultValue }}
22 |
23 | {% elif var.annotations.isLayout %}
24 | {{ type.accessLevel }} var layout: ComponentLayout { return {{ type.annotations.ComponentLayout }}(component: self) }
25 |
26 | {% else %}
27 | {{ type.accessLevel }} {% if var.annotations.isWeak %}weak {% endif %}{% if var.isMutable %}var{% else %}let{% endif %} {{ var.name }}: {{ var.typeName }}
28 |
29 | {% endif %}
30 | {% endfor %}
31 | {{ type.accessLevel }} var identity: {{ type.name }} { return self }
32 |
33 | // sourcery:end
34 | {% endfor %}
35 |
--------------------------------------------------------------------------------
/Templates/AutoHashableComponent.stencil:
--------------------------------------------------------------------------------
1 | // sourcery:file:AutoHashableComponent+{% if argument.project %}{{ argument.project }}{% else %}Cyanic{%endif %}.swift
2 | // swiftlint:disable all
3 | {% macro combineVariableHashes variables %}
4 | {% for variable in variables where variable.readAccess != "private" and variable.readAccess != "fileprivate" %}
5 | {% if not variable.annotations.skipHashing %}
6 | {% if variable.isStatic %}type(of: self).{% else %}self.{% endif %}{{ variable.name }}.hash(into: &hasher)
7 | {% endif %}
8 | {% endfor %}
9 | {% endmacro %}
10 | // MARK: - AutoHashableComponent
11 | {% for type in types.types|!enum where type|annotated:"AutoHashableComponent" %}
12 | {% if argument.isFramework == false and type.annotations.isFrameworkComponent %}
13 | {% else %}
14 | // MARK: - {{ type.annotations.Component }} AutoHashableComponent
15 | extension {{ type.annotations.Component }}: Hashable {
16 | {{ type.accessLevel }}{% if type.based.NSObject or type.supertype.implements.AutoHashableComponent or type.supertype|annotated:"AutoHashableComponent" or type.supertype.based.Hashable %} override{% endif %} func hash(into hasher: inout Hasher) {
17 | {% if type.based.NSObject or type.supertype.implements.AutoHashableComponent or type.supertype|annotated:"AutoHashableComponent" or type.supertype.based.Hashable %}
18 | super.hash(into: hasher)
19 | {% endif %}
20 | {% if not type.kind == "protocol" %}
21 | {% call combineVariableHashes type.storedVariables %}
22 | {% else %}
23 | {% call combineVariableHashes type.allVariables %}
24 | {% endif %}
25 | }
26 | }
27 | {% endif %}
28 | {% endfor %}
29 | // sourcery:end
30 |
--------------------------------------------------------------------------------
/Sources/Components/ChildVCComponent/ChildVCComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 07.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension,RequiredVariables
10 | // sourcery: ComponentLayout = "ChildVCComponentLayout"
11 | /// A ChildVCComponent is a Component that represents a child UIViewController presented on a UICollectionViewCell.
12 | public struct ChildVCComponent: ChildVCComponentType {
13 |
14 | // sourcery:inline:auto:ChildVCComponent.AutoGenerateComponent
15 | /**
16 | Work around Initializer because memberwise initializers are all or nothing
17 | - Parameters:
18 | - id: The unique identifier of the ChildVCComponent.
19 | */
20 | public init(id: String) {
21 | self.id = id
22 | }
23 |
24 | // sourcery: skipHashing, skipEquality
25 | public lazy var childVC: UIViewController & CyanicChildVCType = InvalidChildComponentVC()
26 |
27 | // sourcery: skipHashing, skipEquality
28 | public weak var parentVC: UIViewController?
29 |
30 | // sourcery: skipHashing, skipEquality
31 | public var configuration: (UIViewController) -> Void = { _ in }
32 |
33 | // sourcery: skipHashing, skipEquality
34 | public var insets: UIEdgeInsets = UIEdgeInsets.zero
35 |
36 | public var id: String
37 |
38 | public var width: CGFloat = 0.0
39 |
40 | public var height: CGFloat = 44.0
41 |
42 | // sourcery: skipHashing, skipEquality
43 | public var layout: ComponentLayout { return ChildVCComponentLayout(component: self) }
44 |
45 | public var identity: ChildVCComponent { return self }
46 | // sourcery:end
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/Classes/AnyComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import Differentiator
8 | import Foundation
9 |
10 | /**
11 | Type-erased wrapper for a Component instance
12 | */
13 | public final class AnyComponent: IdentifiableType {
14 |
15 | /**
16 | Initializer.
17 | Keeps the underlying Component in memory and creates a reference to its layout and cellType.
18 | - Parameters:
19 | - component: The Component instance to be type erased.
20 | */
21 | public init(_ component: C) {
22 | self.identity = AnyHashable(component.identity)
23 | self.id = component.id
24 | }
25 |
26 | /**
27 | The layout from the Component.
28 | */
29 | public var layout: ComponentLayout {
30 | return (self.identity.base as! UserInterfaceModel).layout // swiftlint:disable:this force_cast
31 | }
32 |
33 | /**
34 | The underlying Component instance wrapped in an AnyHashable type erased container.
35 | */
36 | public let identity: AnyHashable
37 |
38 | /**
39 | The unique identifier of the Component.
40 | */
41 | public let id: String
42 |
43 | }
44 |
45 | extension AnyComponent: Hashable {
46 |
47 | public static func == (lhs: AnyComponent, rhs: AnyComponent) -> Bool {
48 | return lhs.identity == rhs.identity
49 | }
50 |
51 | public func hash(into hasher: inout Hasher) {
52 | hasher.combine(self.identity)
53 | }
54 |
55 | }
56 |
57 | extension AnyComponent: CustomStringConvertible {
58 |
59 | public var description: String {
60 | return self.identity.description
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/Example/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 |
--------------------------------------------------------------------------------
/Sources/Components/SizedComponent/SizedComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.05.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension
11 | // sourcery: ComponentLayout = SizedComponentLayout
12 | /// SizedComponent is a Component that represents a fixed size UIView.
13 | public struct SizedComponent: SizedComponentType {
14 |
15 | // sourcery:inline:auto:SizedComponent.AutoGenerateComponent
16 | /**
17 | Work around Initializer because memberwise initializers are all or nothing
18 | - Parameters:
19 | - id: The unique identifier of the SizedComponent.
20 | */
21 | public init(id: String) {
22 | self.id = id
23 | }
24 |
25 | public var id: String
26 |
27 | public var width: CGFloat = 0.0
28 |
29 | // sourcery: skipHashing, skipEquality
30 | public var insets: UIEdgeInsets = UIEdgeInsets.zero
31 |
32 | public var backgroundColor: UIColor = UIColor.clear
33 |
34 | // sourcery: skipHashing, skipEquality
35 | public var alignment: Alignment = Alignment.fill
36 |
37 | // sourcery: skipHashing, skipEquality
38 | public var flexibility: Flexibility = Flexibility.flexible
39 |
40 | // sourcery: skipHashing, skipEquality
41 | public var configuration: (UIView) -> Void = { _ in }
42 |
43 | // sourcery: skipHashing, skipEquality
44 | public var viewClass: UIView.Type = UIView.self
45 |
46 | public var height: CGFloat = 44.0
47 |
48 | // sourcery: skipHashing, skipEquality
49 | public var layout: ComponentLayout { return SizedComponentLayout(component: self) }
50 |
51 | public var identity: SizedComponent { return self }
52 | // sourcery:end
53 | }
54 |
--------------------------------------------------------------------------------
/Templates/CyanicComponent.xctemplate/___FILEBASENAME___Component.swift:
--------------------------------------------------------------------------------
1 | import Cyanic
2 | import LayoutKit
3 | import UIKit
4 |
5 | // sourcery: AutoEquatableComponent,AutoHashableComponent
6 | // sourcery: Component = ___VARIABLE_productName:identifier___Component
7 | public protocol ___VARIABLE_productName:identifier___ComponentType: StaticHeightComponent {
8 |
9 | // define here your component properties and annotations and run sourcery
10 | // sourcery: defaultValue = "UIColor.clear"
11 | var backgroundColor: UIColor { get set }
12 |
13 | }
14 |
15 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension
16 | // sourcery: ComponentLayout = ___VARIABLE_productName:identifier___ComponentLayout
17 | public struct ___VARIABLE_productName:identifier___Component: ___VARIABLE_productName:identifier___ComponentType {
18 | // left initially empty
19 | }
20 |
21 | public final class ___VARIABLE_productName:identifier___ComponentLayout: SizeLayout, ComponentLayout {
22 |
23 | public init(component: ___VARIABLE_productName:identifier___Component) {
24 | let size: CGSize = CGSize(width: component.width, height: component.height)
25 |
26 | #warning("Please consider that sublayout parameter must not be nil")
27 | super.init(
28 | minWidth: size.width,
29 | maxWidth: size.width,
30 | minHeight: size.height,
31 | maxHeight: size.height,
32 | alignment: Alignment.fill,
33 | flexibility: Flexibility.flexible,
34 | viewReuseId: ___VARIABLE_productName:identifier___ComponentLayout.identifier,
35 | sublayout: nil,
36 | config: { (view: UIView) -> Void in
37 | view.backgroundColor = component.backgroundColor
38 | }
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Components/StaticLabelComponent/StaticLabelComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 25.08.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | /**
11 | The StaticLabelComponentLayout is a ComponentLayout that is a subclass of SizeLayout.
12 | Arranges the content of the StaticLabelComponent.
13 | */
14 | public final class StaticLabelComponentLayout: SizeLayout, ComponentLayout {
15 |
16 | public init(component: StaticLabelComponent) {
17 | let labelLayout: LabelLayout = LabelLayout(
18 | text: component.text,
19 | font: component.font,
20 | numberOfLines: component.numberOfLines,
21 | lineBreakMode: component.lineBreakMode,
22 | alignment: component.alignment,
23 | flexibility: component.flexibility,
24 | viewReuseId: "\(StaticLabelComponentLayout.identifier)Label",
25 | config: component.configuration
26 | )
27 |
28 | let insetLayout: InsetLayout = InsetLayout(insets: component.insets, sublayout: labelLayout)
29 | let height: CGFloat = insetLayout
30 | .measurement(within: CGSize(width: component.width, height: CGFloat.greatestFiniteMagnitude))
31 | .size
32 | .height
33 |
34 | let width: CGFloat = component.width
35 |
36 | super.init(
37 | minWidth: width,
38 | maxWidth: width,
39 | minHeight: height,
40 | maxHeight: height,
41 | viewReuseId: "\(StaticTextComponentLayout.identifier)Size",
42 | sublayout: insetLayout,
43 | config: {
44 | $0.backgroundColor = component.backgroundColor
45 | }
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/Components/StaticTextComponent/StaticTextComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension
11 | // sourcery: ComponentLayout = StaticTextComponentLayout
12 | /// StaticTextComponent is a Component that represents static text to be displayed in a UICollectionViewCell.
13 | public struct StaticTextComponent: StaticTextComponentType {
14 |
15 | // sourcery:inline:auto:StaticTextComponent.AutoGenerateComponent
16 | /**
17 | Work around Initializer because memberwise initializers are all or nothing
18 | - Parameters:
19 | - id: The unique identifier of the StaticTextComponent.
20 | */
21 | public init(id: String) {
22 | self.id = id
23 | }
24 |
25 | public var id: String
26 |
27 | public var width: CGFloat = 0.0
28 |
29 | public var text: Text = Text.unattributed("")
30 |
31 | public var font: UIFont = UIFont.systemFont(ofSize: 13.0)
32 |
33 | public var backgroundColor: UIColor = UIColor.clear
34 |
35 | public var lineFragmentPadding: CGFloat = 0.0
36 |
37 | // sourcery: skipHashing, skipEquality
38 | public var insets: UIEdgeInsets = UIEdgeInsets.zero
39 |
40 | // sourcery: skipHashing, skipEquality
41 | public var alignment: Alignment = Alignment.centerLeading
42 |
43 | // sourcery: skipHashing, skipEquality
44 | public var flexibility: Flexibility = TextViewLayoutDefaults.defaultFlexibility
45 |
46 | // sourcery: skipHashing, skipEquality
47 | public var configuration: (UITextView) -> Void = { _ in }
48 |
49 | // sourcery: skipHashing, skipEquality
50 | public var layout: ComponentLayout { return StaticTextComponentLayout(component: self) }
51 |
52 | public var identity: StaticTextComponent { return self }
53 | // sourcery:end
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/Components/Protocols/SizedComponentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.05.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoEquatableComponent,AutoHashableComponent
11 | // sourcery: Component = SizedComponent,isFrameworkComponent
12 | /// StaticTextComponentType is a protocol for Component data structures that represent UI of a fixed size.
13 | public protocol SizedComponentType: StaticHeightComponent {
14 |
15 | // sourcery: defaultValue = UIEdgeInsets.zero
16 | // sourcery: skipHashing, skipEquality
17 | /// The insets on the UIView relative to its root UIView.
18 | /// The default value is UIEdgeInsets.zero.
19 | var insets: UIEdgeInsets { get set }
20 |
21 | // sourcery: defaultValue = UIColor.clear
22 | /// The background color of the root view. The default value is UIColor.clear.
23 | var backgroundColor: UIColor { get set }
24 |
25 | // sourcery: defaultValue = Alignment.fill
26 | // sourcery: skipHashing, skipEquality
27 | /// The alignment of the underlying SizeLayout. The default value is Alignment.fill.
28 | var alignment: Alignment { get set }
29 |
30 | // sourcery: defaultValue = Flexibility.flexible
31 | // sourcery: skipHashing, skipEquality
32 | /// The flexibility of the underlying SizeLayout. The default value is Flexibility.flexible.
33 | var flexibility: Flexibility { get set }
34 |
35 | // sourcery: defaultValue = "{ _ in }"
36 | // sourcery: skipHashing, skipEquality
37 | /// The configuration applied to the UIView. The default closure does nothing.
38 | var configuration: (UIView) -> Void { get set }
39 |
40 | // sourcery: defaultValue = "UIView.self"
41 | // sourcery: skipHashing, skipEquality
42 | /// The UIView subclass that will be created to fill the space of the root UIView.
43 | var viewClass: UIView.Type { get set }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/Cyanic.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 |
3 | spec.name = "Cyanic"
4 | spec.version = "0.9.2"
5 | spec.summary = "Cyanic is a MvRx and Epoxy inspired framework that aims to build a reactive UI in a UICollectionView/UITableView."
6 |
7 | spec.description = <<-DESC
8 | Cyanic is a MvRx and Epoxy inspired framework that aims to build reactive UI in a UICollectionView/UITableView.
9 | It borrows heavily from MvRx in terms of API and structure while falling within the constraints of
10 | Swift and iOS development. It leverages RxSwift to have reactive functionality, LayoutKit to have
11 | performance close to manual layout when sizing and arranging subviews, and Sourcery for fast creation of
12 | custom components. It uses an Model-View-ViewModel (MVVM) style of architecture.
13 | DESC
14 |
15 | spec.homepage = "https://github.com/feilfeilundfeil/Cyanic"
16 | spec.license = { :type => "MIT", :file => "LICENSE" }
17 | spec.authors = "Feil, Feil, & Feil GmbH", "Julio Alorro", "Jonas Bark"
18 | spec.ios.deployment_target = "10.0"
19 | spec.source = { :git => "https://github.com/feilfeilundfeil/Cyanic.git", :tag => spec.version }
20 | spec.source_files = "Sources/**/*.swift", "Sources/Components/**/*.swift"
21 | spec.resources = ["Templates/*"]
22 | spec.requires_arc = true
23 | spec.swift_version = "5.0"
24 |
25 | spec.dependency "LayoutKit"
26 | spec.dependency "RxDataSources"
27 | spec.dependency "RxSwift"
28 | spec.dependency "Sourcery"
29 |
30 | spec.test_spec "Tests" do |test_spec|
31 | test_spec.source_files = "Tests/*.swift"
32 | test_spec.dependency "Quick"
33 | test_spec.dependency "Nimble"
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/Sources/Components/SizedComponent/SizedComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.05.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 |
9 | /**
10 | The SizedComponentLayout is a ComponentLayout that is a subclass of SizeLayout.
11 | Used to create, size, and arrange the subviews associated with SizedComponent.
12 | */
13 | public final class SizedComponentLayout: SizeLayout, ComponentLayout {
14 |
15 | public init(component: SizedComponent) {
16 | let insets: UIEdgeInsets = component.insets
17 | let sizeLayout: SizeLayout = SizeLayout(
18 | minWidth: component.width,
19 | maxWidth: component.width,
20 | minHeight: component.height,
21 | maxHeight: component.height,
22 | alignment: component.alignment,
23 | viewReuseId: "\(SizedComponentLayout.identifier)ViewSize - \(component.id)",
24 | viewClass: component.viewClass,
25 | sublayout: nil,
26 | config: component.configuration
27 | )
28 |
29 | let insetLayout: InsetLayout = InsetLayout(
30 | insets: insets,
31 | flexibility: Flexibility.inflexible,
32 | viewReuseId: "\(SizedComponentLayout.identifier)Inset - \(component.id)",
33 | sublayout: sizeLayout
34 | )
35 |
36 | super.init(
37 | minWidth: component.width,
38 | maxWidth: component.width,
39 | minHeight: component.height,
40 | maxHeight: component.height,
41 | alignment: component.alignment,
42 | flexibility: component.flexibility,
43 | viewReuseId: "\(SizedComponentLayout.identifier)Size - \(component.id)",
44 | sublayout: insetLayout,
45 | config: { (view: UIView) -> Void in
46 | view.backgroundColor = component.backgroundColor
47 | }
48 | )
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Alacrity (0.5.4)
3 | - Differentiator (4.0.1)
4 | - Kio (2.2.2)
5 | - LayoutKit (10.1.4)
6 | - Nimble (8.0.4)
7 | - Quick (2.2.0)
8 | - RxCocoa (5.0.1):
9 | - RxRelay (~> 5)
10 | - RxSwift (~> 5)
11 | - RxDataSources (4.0.1):
12 | - Differentiator (~> 4.0)
13 | - RxCocoa (~> 5.0)
14 | - RxSwift (~> 5.0)
15 | - RxRelay (5.0.1):
16 | - RxSwift (~> 5)
17 | - RxSwift (5.0.1)
18 | - SideMenu (6.4.7)
19 | - Sourcery (0.17.0)
20 |
21 | DEPENDENCIES:
22 | - Alacrity
23 | - Kio
24 | - LayoutKit (from `https://github.com/hooliooo/LayoutKit.git`)
25 | - Nimble
26 | - Quick
27 | - RxDataSources
28 | - RxSwift
29 | - SideMenu
30 | - Sourcery
31 |
32 | SPEC REPOS:
33 | https://github.com/CocoaPods/Specs.git:
34 | - Alacrity
35 | - Differentiator
36 | - Kio
37 | - Nimble
38 | - Quick
39 | - RxCocoa
40 | - RxDataSources
41 | - RxRelay
42 | - SideMenu
43 | - Sourcery
44 | trunk:
45 | - RxSwift
46 |
47 | EXTERNAL SOURCES:
48 | LayoutKit:
49 | :git: https://github.com/hooliooo/LayoutKit.git
50 |
51 | CHECKOUT OPTIONS:
52 | LayoutKit:
53 | :commit: 72249ccabb4f997127e1414df6aedd5422248fab
54 | :git: https://github.com/hooliooo/LayoutKit.git
55 |
56 | SPEC CHECKSUMS:
57 | Alacrity: f69c0bbe4917912d83a63c981884420f857ddeff
58 | Differentiator: 886080237d9f87f322641dedbc5be257061b0602
59 | Kio: 320b6cb88a8c9175bb72880e8a85bea66027a0a8
60 | LayoutKit: 59016c1f920c022e39dc984e7d593ae8a503c8e5
61 | Nimble: 18d5360282923225d62b09d781f63abc1a0111fc
62 | Quick: 7fb19e13be07b5dfb3b90d4f9824c855a11af40e
63 | RxCocoa: e741b9749968e8a143e2b787f1dfbff2b63d0a5c
64 | RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114
65 | RxRelay: 89d54507f4fd4d969e6ec1d4bd7f3673640b4640
66 | RxSwift: e2dc62b366a3adf6a0be44ba9f405efd4c94e0c4
67 | SideMenu: 928a015669c3afc201e1fe3a87f1b0bc8993a664
68 | Sourcery: 3ed61be7c8a1218fce349266139379dba477efe0
69 |
70 | PODFILE CHECKSUM: 22099bc707db07b0dcefe78335a992b98d8eb9f9
71 |
72 | COCOAPODS: 1.8.4
73 |
--------------------------------------------------------------------------------
/Sources/Components/ButtonComponent/ButtonComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 27.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension
11 | // sourcery: ComponentLayout = ButtonComponentLayout
12 | /// ButtonComponent is a Component that represents a UIButton.
13 | public struct ButtonComponent: ButtonComponentType {
14 |
15 | // sourcery:inline:auto:ButtonComponent.AutoGenerateComponent
16 | /**
17 | Work around Initializer because memberwise initializers are all or nothing
18 | - Parameters:
19 | - id: The unique identifier of the ButtonComponent.
20 | */
21 | public init(id: String) {
22 | self.id = id
23 | }
24 |
25 | public var type: ButtonLayoutType = ButtonLayoutType.system
26 |
27 | public var title: String = ""
28 |
29 | public var font: UIFont = UIFont.systemFont(ofSize: 15.0)
30 |
31 | // sourcery: skipHashing, skipEquality
32 | public var insets: UIEdgeInsets = UIEdgeInsets.zero
33 |
34 | public var backgroundColor: UIColor = UIColor.clear
35 |
36 | // sourcery: skipHashing, skipEquality
37 | public var alignment: Alignment = ButtonLayoutDefaults.defaultAlignment
38 |
39 | // sourcery: skipHashing, skipEquality
40 | public var flexibility: Flexibility = ButtonLayoutDefaults.defaultFlexibility
41 |
42 | // sourcery: skipHashing, skipEquality
43 | public var configuration: (UIButton) -> Void = { _ in }
44 |
45 | // sourcery: skipHashing, skipEquality
46 | public var onTap: (UIButton) -> Void = { _ in print("Hello World \(#file)") }
47 |
48 | public var id: String
49 |
50 | public var width: CGFloat = 0.0
51 |
52 | public var height: CGFloat = 44.0
53 |
54 | // sourcery: skipHashing, skipEquality
55 | public var layout: ComponentLayout { return ButtonComponentLayout(component: self) }
56 |
57 | public var identity: ButtonComponent { return self }
58 | // sourcery:end
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Components/StaticLabelComponent/StaticLabelComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 25.08.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension
11 | // sourcery: ComponentLayout = StaticLabelComponentLayout
12 | /// A StaticLabelComponent is a Component that represents a UICollectionViewCell/UITableViewCell that displays a
13 | /// static text on a UILabel in its contentView.
14 | public struct StaticLabelComponent: StaticLabelComponentType {
15 |
16 | // sourcery:inline:auto:StaticLabelComponent.AutoGenerateComponent
17 | /**
18 | Work around Initializer because memberwise initializers are all or nothing
19 | - Parameters:
20 | - id: The unique identifier of the StaticLabelComponent.
21 | */
22 | public init(id: String) {
23 | self.id = id
24 | }
25 |
26 | public var id: String
27 |
28 | public var width: CGFloat = 0.0
29 |
30 | public var text: Text = Text.unattributed("")
31 |
32 | public var font: UIFont = UIFont.systemFont(ofSize: 13.0)
33 |
34 | public var backgroundColor: UIColor = UIColor.clear
35 |
36 | public var numberOfLines: Int = 0
37 |
38 | public var lineBreakMode: NSLineBreakMode = NSLineBreakMode.byTruncatingTail
39 |
40 | // sourcery: skipHashing, skipEquality
41 | public var insets: UIEdgeInsets = UIEdgeInsets.zero
42 |
43 | // sourcery: skipHashing, skipEquality
44 | public var alignment: Alignment = Alignment.centerLeading
45 |
46 | // sourcery: skipHashing, skipEquality
47 | public var flexibility: Flexibility = Flexibility.flexible
48 |
49 | // sourcery: skipHashing, skipEquality
50 | public var configuration: (UILabel) -> Void = { _ in }
51 |
52 | // sourcery: skipHashing, skipEquality
53 | public var layout: ComponentLayout { return StaticLabelComponentLayout(component: self) }
54 |
55 | public var identity: StaticLabelComponent { return self }
56 | // sourcery:end
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/Components/StaticTextComponent/StaticTextComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | /**
11 | The StaticTextComponentLayout is a ComponentLayout that is a subclass of SizeLayout.
12 | Used to create, size, and arrange the subviews associated with StaticTextComponent.
13 | */
14 | public final class StaticTextComponentLayout: SizeLayout, ComponentLayout {
15 |
16 | /**
17 | Initializer.
18 | - Parameters:
19 | - component: The StaticTextComponent whose properties define the UI characters of the subviews to be created.
20 | */
21 | public init(component: StaticTextComponent) {
22 | let textLayout: TextViewLayout = TextViewLayout(
23 | text: component.text,
24 | font: component.font,
25 | lineFragmentPadding: component.lineFragmentPadding,
26 | textContainerInset: component.insets,
27 | layoutAlignment: component.alignment,
28 | flexibility: component.flexibility,
29 | viewReuseId: "\(StaticTextComponentLayout.identifier)TextView",
30 | config: { (view: UITextView) -> Void in
31 | view.backgroundColor = UIColor.clear
32 | view.isEditable = false
33 | view.isScrollEnabled = false
34 | component.configuration(view)
35 | }
36 | )
37 |
38 | let size: CGSize = CGSize(width: component.width, height: CGFloat.greatestFiniteMagnitude)
39 |
40 | super.init(
41 | minWidth: size.width,
42 | maxWidth: size.width,
43 | minHeight: 0.0,
44 | maxHeight: size.height,
45 | viewReuseId: "\(StaticTextComponentLayout.identifier)Size",
46 | sublayout: textLayout,
47 | config: {
48 | $0.backgroundColor = component.backgroundColor
49 | }
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Pods/Pods.xcodeproj/xcuserdata
2 | *.DS_Store
3 |
4 | # Created by https://www.gitignore.io/api/xcode,swift
5 |
6 | ### Swift ###
7 | # Xcode
8 | #
9 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
10 |
11 | ## Build generated
12 | build/
13 | DerivedData/
14 |
15 | ## Various settings
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 | xcuserdata/
25 |
26 | ## Other
27 | *.moved-aside
28 | *.xccheckout
29 | *.xcscmblueprint
30 |
31 | ## Obj-C/Swift specific
32 | *.hmap
33 | *.ipa
34 | *.dSYM.zip
35 | *.dSYM
36 |
37 | ## Playgrounds
38 | timeline.xctimeline
39 | playground.xcworkspace
40 |
41 | # Swift Package Manager
42 | #
43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
44 | # Packages/
45 | # Package.pins
46 | .build/
47 |
48 | # CocoaPods
49 | #
50 | # We recommend against adding the Pods directory to your .gitignore. However
51 | # you should judge for yourself, the pros and cons are mentioned at:
52 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
53 | #
54 | Pods/
55 |
56 | # Carthage
57 | #
58 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
59 | # Carthage/Checkouts
60 |
61 | Carthage/Build
62 |
63 | # fastlane
64 | #
65 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
66 | # screenshots whenever they are needed.
67 | # For more information about the recommended setup visit:
68 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
69 |
70 | fastlane/report.xml
71 | fastlane/Preview.html
72 | fastlane/screenshots
73 | fastlane/test_output
74 |
75 |
76 | ### Xcode ###
77 | build
78 | ##*.xcodeproj/*
79 | !*.xcodeproj/project.pbxproj
80 | !*.xcworkspace/contents.xcworkspacedata
81 | /*.gcno
82 |
83 | .idea/
84 |
85 | # End of https://www.gitignore.io/api/xcode,swift
86 |
--------------------------------------------------------------------------------
/Sources/Protocols/StateObservableBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 23.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import RxSwift
8 |
9 | internal protocol StateObservableBuilder {
10 |
11 | /**
12 | CombineState represents the aggregate of the Observables being monitored for new values.
13 | */
14 | associatedtype CombinedState
15 |
16 | @discardableResult
17 | func setUpObservables(with viewModels: [AnyViewModel]) -> Observable
18 |
19 | }
20 |
21 | internal extension StateObservableBuilder {
22 |
23 | /**
24 | Throttles or debounces the observable based on the passed ThrottleType argument.
25 |
26 | This method either throttles or debounces the observable unless ThrottleType.none is passed.
27 | - Parameters:
28 | - observable: The Observable instance to be throttled, debounced ,or unchanged.
29 | - throttleType: The type of throttling to be used on the the Observable.
30 | - scheduler: The SchedulerType where the throttling / debouncing occurs.
31 | - Returns:
32 | The throttled Observable if ThrottleType.debounce or ThrottleType.throttle are used. Otherwise, it returns
33 | the same observable.
34 | */
35 | func setUpThrottleType(
36 | on observable: Observable,
37 | throttleType: ThrottleType,
38 | scheduler: SchedulerType
39 | ) -> Observable {
40 | let throttledObservable: Observable
41 |
42 | switch throttleType {
43 | case .debounce(let timeInterval):
44 | throttledObservable = observable
45 | .debounce(timeInterval, scheduler: scheduler)
46 |
47 | case .throttle(let timeInterval):
48 | throttledObservable = observable
49 | .throttle(timeInterval, latest: true, scheduler: scheduler)
50 |
51 | case .none:
52 | throttledObservable = observable
53 | }
54 |
55 | return throttledObservable
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/Components/Protocols/ChildVCComponentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 07.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | // sourcery: AutoEquatableComponent,AutoHashableComponent
10 | // sourcery: Component = ChildVCComponent,isFrameworkComponent
11 | /// ChildVCComponentType is a protocol for Component data structures that want to show other UIViewControllers as a
12 | /// child UIViewController to the SingleSectionComponentViewController.
13 | public protocol ChildVCComponentType: StaticHeightComponent {
14 |
15 | // sourcery: skipHashing, skipEquality, isLazy
16 | // sourcery: defaultValue = "InvalidChildComponentVC()"
17 | /// The child UIViewController instance to be shown on the UICollectionView. This must be a lazy property otherwise you
18 | /// may run into threading issues.
19 | var childVC: UIViewController & CyanicChildVCType { mutating get set }
20 |
21 | // sourcery: skipHashing, skipEquality
22 | // sourcery: isWeak
23 | /// The parent UIViewController instance of the child VC. It is usually the BaseComponentVC.
24 | var parentVC: UIViewController? { get set }
25 |
26 | // sourcery: skipHashing, skipEquality
27 | // sourcery: defaultValue = "{ _ in }"
28 | /// Additional configuration of the childVC. Called in the Layout's config closure
29 | var configuration: (UIViewController) -> Void { get set }
30 |
31 | // sourcery: skipHashing, skipEquality
32 | // sourcery: defaultValue = "UIEdgeInsets.zero"
33 | /// The insets of the child VC. The default value is UIEdgeInsets.zero.
34 | var insets: UIEdgeInsets { get set }
35 |
36 | }
37 |
38 | public extension ChildVCComponentType {
39 |
40 | /// The class name of the childVC.
41 | // sourcery: isExcluded
42 | var name: String {
43 | var component: Self = self
44 | return String(describing: Mirror(reflecting: component.childVC).subjectType)
45 | }
46 |
47 | }
48 |
49 | public extension ChildVCComponentType {
50 |
51 | // sourcery: isExcluded
52 | var description: String {
53 | return self.name
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/Classes/AbstractViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 04.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import RxCocoa
8 | import RxSwift
9 | import os
10 |
11 | internal let CyanicViewModelLog: OSLog = OSLog(subsystem: Constants.bundleIdentifier, category: "ViewModel")
12 |
13 | /**
14 | AbstractViewModel is a class that provides the essential functionality that must exist in all ViewModel subclasses.
15 | */
16 | open class AbstractViewModel: NSObject, ViewModelType {
17 |
18 | /**
19 | Initializer for the ViewModel.
20 | When instantiating the ViewModel, it is important to pass an initial State object which should represent
21 | the initial State of the current view / screen of the app.
22 | - Parameters:
23 | - initialState: The starting State of the ViewModel.
24 | */
25 | public init(initialState: StateType, isDebugMode: Bool = false) {
26 | self.stateStore = StateStore(initialState: initialState)
27 | self.isDebugMode = isDebugMode
28 | super.init()
29 | }
30 |
31 | deinit {
32 | logDeallocation(of: self, log: CyanicViewModelLog)
33 | }
34 |
35 | /**
36 | The StateStore that manages the State of the ViewModel
37 | */
38 | internal let stateStore: StateStore
39 |
40 | /**
41 | Indicates whether debugging functionality will be used.
42 | */
43 | internal let isDebugMode: Bool
44 |
45 | /**
46 | The DisposeBag used to clean up any Rx related subscriptions related to the ViewModel instance.
47 | */
48 | public let disposeBag: DisposeBag = DisposeBag()
49 |
50 | /**
51 | Accessor for the current State of the AbstractViewModel.
52 | */
53 | public var currentState: StateType {
54 | return self.stateStore.currentState
55 | }
56 |
57 | }
58 |
59 | public extension AbstractViewModel {
60 |
61 | /**
62 | Accessor for the State Observable of the AbstractViewModel.
63 | */
64 | var state: Observable {
65 | return self.stateStore.state
66 | }
67 |
68 | }
69 |
70 | internal protocol ViewModelType {}
71 |
--------------------------------------------------------------------------------
/Templates/AutoEquatableComponent.stencil:
--------------------------------------------------------------------------------
1 | // sourcery:file:AutoEquatableComponent+{% if argument.project %}{{ argument.project }}{% else %}Cyanic{%endif %}.swift
2 | // swiftlint:disable all
3 | fileprivate func compareOptionals(lhs: T?, rhs: T?, compare: (_ lhs: T, _ rhs: T) -> Bool) -> Bool {
4 | switch (lhs, rhs) {
5 | case let (lValue?, rValue?):
6 | return compare(lValue, rValue)
7 | case (nil, nil):
8 | return true
9 | default:
10 | return false
11 | }
12 | }
13 |
14 | fileprivate func compareArrays(lhs: [T], rhs: [T], compare: (_ lhs: T, _ rhs: T) -> Bool) -> Bool {
15 | guard lhs.count == rhs.count else { return false }
16 | for (idx, lhsItem) in lhs.enumerated() {
17 | guard compare(lhsItem, rhs[idx]) else { return false }
18 | }
19 |
20 | return true
21 | }
22 |
23 | {% macro compareVariables variables %}
24 | {% for variable in variables where variable.readAccess != "private" and variable.readAccess != "fileprivate" %}{% if not variable.annotations.skipEquality %}guard {% if not variable.isOptional %}{% if not variable.annotations.arrayEquality %}lhs.{{ variable.name }} == rhs.{{ variable.name }}{% else %}compareArrays(lhs: lhs.{{ variable.name }}, rhs: rhs.{{ variable.name }}, compare: ==){% endif %}{% else %}compareOptionals(lhs: lhs.{{ variable.name }}, rhs: rhs.{{ variable.name }}, compare: ==){% endif %} else { return false }{% endif %}
25 | {% endfor %}
26 | {% endmacro %}
27 | // MARK: - AutoEquatableComponent
28 | {% for type in types.types|!enum where type|annotated:"AutoEquatableComponent" %}
29 | {% if argument.isFramework == false and type.annotations.isFrameworkComponent %}
30 | {% else %}
31 | // MARK: - {{ type.annotations.Component }} AutoEquatableComponent
32 | extension {{ type.annotations.Component }}: Equatable {}
33 | {{ type.accessLevel }} func == (lhs: {{ type.annotations.Component }}, rhs: {{ type.annotations.Component }}) -> Bool {
34 | {% if not type.kind == "protocol" %}
35 | {% call compareVariables type.storedVariables %}
36 | {% else %}
37 | {% call compareVariables type.allVariables %}
38 | {% endif %}
39 | return true
40 | }
41 | {% endif %}
42 | {% endfor %}
43 | // sourcery:end
44 |
--------------------------------------------------------------------------------
/Sources/TableView/TableComponentViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | /**
10 | TableComponentViewController is a subclass of ComponentViewController. It serves as the base class for the
11 | SingleSectionTableComponentViewController and MultiSectionTableComponentViewController, therefore it contains
12 | the logic and implementations shared between the two subclasses.
13 | */
14 | open class TableComponentViewController: ComponentViewController, UITableViewDelegate {
15 |
16 | // MARK: UIViewController Lifecycle Methods
17 | open override func viewDidLoad() {
18 | super.viewDidLoad()
19 |
20 | // Register TableComponentCell
21 | self.tableView.register(TableComponentCell.self, forCellReuseIdentifier: TableComponentCell.identifier)
22 |
23 | // Set up as the UITableView's UITableViewDelegate
24 | self.tableView.delegate = self
25 | }
26 |
27 | // MARK: Views
28 | /**
29 | The UITableView instance managed by this CollectionComponentViewController instance.
30 | */
31 | public var tableView: UITableView {
32 | return self._listView as! UITableView // swiftlint:disable:this force_cast
33 | }
34 |
35 | // MARK: Methods
36 | /**
37 | Creates a UITableView with a UITableView.Style of plain. This method is called in the ComponentViewController's
38 | loadView method.
39 | - Returns:
40 | - UITableView instance typed as a UIView.
41 | */
42 | open override func setUpListView() -> UIView {
43 | return UITableView(
44 | frame: CGRect.zero,
45 | style: UITableView.Style.plain
46 | )
47 | }
48 |
49 | // MARK: UITableViewDelegate Methods
50 | open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
51 | tableView.deselectRow(at: indexPath, animated: true)
52 | guard let component: AnyComponent = self.component(at: indexPath) else { return }
53 | guard let selectable = component.identity.base as? Selectable else { return }
54 | guard let cell = tableView.cellForRow(at: indexPath) else { return }
55 | selectable.onSelect(cell.contentView)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/TableView/TableComponentCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | /**
11 | TableComponentCell serves as the root UIView for the UI elements generated by its Layout
12 | */
13 | open class TableComponentCell: UITableViewCell {
14 |
15 | /**
16 | The String identifier used by the TableComponentCell to register to a UITableView instance
17 | */
18 | open class var identifier: String {
19 | return String(describing: Mirror(reflecting: self).subjectType)
20 | }
21 |
22 | // MARK: Layout
23 | /**
24 | The current ComponentLayout instance that created and arranged the subviews in the contentView of this TableComponentCell.
25 | */
26 | private var layout: ComponentLayout?
27 |
28 | override open func prepareForReuse() {
29 | super.prepareForReuse()
30 | self.layout = nil
31 | }
32 |
33 | override public final func sizeThatFits(_ size: CGSize) -> CGSize {
34 | guard let size = self.layout?.measurement(within: size).size else { return CGSize.zero }
35 | return size
36 | }
37 |
38 | override public final var intrinsicContentSize: CGSize {
39 | return self.sizeThatFits(
40 | CGSize(
41 | width: self.bounds.width,
42 | height: self.bounds.height
43 | )
44 | )
45 | }
46 |
47 | /**
48 | Reads the layout from the AnyComponent instance to create the subviews in this TableComponentCell instance. This also
49 | sets the contentView.frame.size to the cell's intrinsicContentSize and calls setNeedsLayout.
50 | - Parameters:
51 | - component: The AnyComponent instance that represents this TableComponentCell
52 | */
53 | open func configure(with component: AnyComponent) {
54 | self.layout = component.layout
55 | self.contentView.frame.size = self.intrinsicContentSize
56 |
57 | self.layout?.arrangement(
58 | origin: self.contentView.bounds.origin,
59 | width: self.contentView.bounds.size.width,
60 | height: self.contentView.bounds.size.height
61 | )
62 | .makeViews(in: self.contentView)
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/CollectionView/CollectionComponentCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 09.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import Foundation
8 | import LayoutKit
9 | import UIKit
10 |
11 | /**
12 | CollectionComponentCell serves as the root UIView for the UI elements generated by its Layout.
13 | */
14 | open class CollectionComponentCell: UICollectionViewCell {
15 |
16 | /**
17 | The String identifier used by the CollectionComponentCell to register to a UICollectionView instance
18 | */
19 | open class var identifier: String {
20 | return String(describing: Mirror(reflecting: self).subjectType)
21 | }
22 |
23 | // MARK: Layout
24 | /**
25 | The current ComponentLayout instance that created and arranged the subviews in the contentView of this CollectionComponentCell.
26 | */
27 | private var layout: ComponentLayout?
28 |
29 | override open func prepareForReuse() {
30 | super.prepareForReuse()
31 | self.layout = nil
32 | }
33 |
34 | override public final func sizeThatFits(_ size: CGSize) -> CGSize {
35 | guard let size = self.layout?.measurement(within: size).size else { return CGSize.zero }
36 | return size
37 | }
38 |
39 | override public final var intrinsicContentSize: CGSize {
40 | return self.sizeThatFits(
41 | CGSize(
42 | width: self.bounds.width,
43 | height: self.bounds.height
44 | )
45 | )
46 | }
47 |
48 | /**
49 | Reads the layout from the AnyComponent instance to create the subviews in this CollectionComponentCell instance. This also
50 | sets the contentView.frame.size to the cell's intrinsicContentSize and calls setNeedsLayout.
51 | - Parameters:
52 | - component: The AnyComponent instance that represents this CollectionComponentCell
53 | */
54 | open func configure(with component: AnyComponent) {
55 | self.layout = component.layout
56 | self.contentView.frame.size = self.intrinsicContentSize
57 |
58 | self.layout?.arrangement(
59 | origin: self.contentView.bounds.origin,
60 | width: self.contentView.bounds.size.width,
61 | height: self.contentView.bounds.size.height
62 | )
63 | .makeViews(in: self.contentView)
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/Utilities/MultiSectionController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 12.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import CoreGraphics
8 |
9 | /**
10 | MultiSectionController represents the entire data source of a UICollectionView/UITableView. It manages an Array of
11 | SectionControllers.
12 | */
13 | public struct MultiSectionController {
14 |
15 | // MARK: Initializer
16 | /**
17 | Initializer.
18 | - Parameters:
19 | - width: The width of the UICollectionView/UITableView. MultiSectionController initializes its SectionController
20 | instances with it.
21 | */
22 | public init(width: CGFloat) {
23 | self.width = width
24 | }
25 |
26 | // MARK: Stored Properties
27 | /**
28 | The CGFloat of the UICollectionView where the components will be displayed.
29 | */
30 | public let width: CGFloat
31 |
32 | /**
33 | The SectionControllers representing the sections in the UICollectionView.
34 | */
35 | public private(set) var sectionControllers: [SectionController] = []
36 |
37 | // MARK: Computed Properties
38 | /**
39 | The height of the UICollectionView where the components will be displayed.
40 | */
41 | public var height: CGFloat {
42 | return self.sectionControllers.reduce(into: 0.0, { (currentHeight: inout CGFloat, section: SectionController) -> Void in
43 | currentHeight += section.height
44 | })
45 | }
46 |
47 | /**
48 | Creates a SectionController instance and configures its properties with the given closure. You must provide a
49 | sectionComponent, otherwise it will force a fatalError.
50 | - Parameters:
51 | - configuration: The closure that mutates the mutable ButtonComponent.
52 | - sectionController: The SectionController instance to be mutated/configured.
53 | - Returns:
54 | SectionController
55 | */
56 | @discardableResult
57 | public mutating func sectionController(with configuration: (_ sectionController: inout SectionController) -> Void) -> SectionController {
58 | var sectionController: SectionController = SectionController(width: self.width)
59 | configuration(§ionController)
60 | self.sectionControllers.append(sectionController)
61 | return sectionController
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/CollectionView/CyanicNoFadeFlowLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 11.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | /**
10 | The default behavior for UICollectionViewFlowLayout when its UICollectionView updates its cell is to show a "flash",
11 | sometimes this behaviour is acceptable and other times it isn't. This UICollectionViewFlowLayout will keep the alpha
12 | of updated UICollectionViewCells therefore eliminating the "flash".
13 |
14 | **Caveats**
15 |
16 | This shouldn't be used with the ExpandableComponent or any SingleSectionComponentViewController where the elements
17 | are changing. It makes the animation look broken
18 | */
19 | open class CyanicNoFadeFlowLayout: UICollectionViewFlowLayout {
20 |
21 | open override func initialLayoutAttributesForAppearingSupplementaryElement(ofKind elementKind: String, at elementIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
22 | let attributes: UICollectionViewLayoutAttributes? = super
23 | .initialLayoutAttributesForAppearingSupplementaryElement(ofKind: elementKind, at: elementIndexPath)
24 | attributes?.alpha = 1.0
25 | return attributes
26 | }
27 |
28 | open override func finalLayoutAttributesForDisappearingSupplementaryElement(ofKind elementKind: String, at elementIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
29 | let attributes: UICollectionViewLayoutAttributes? = super
30 | .finalLayoutAttributesForDisappearingSupplementaryElement(ofKind: elementKind, at: elementIndexPath)
31 | attributes?.alpha = 1.0
32 | return attributes
33 | }
34 |
35 | override open func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
36 | let attributes: UICollectionViewLayoutAttributes? = super
37 | .initialLayoutAttributesForAppearingItem(at: itemIndexPath)
38 | attributes?.alpha = 1.0
39 | return attributes
40 | }
41 |
42 | override open func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
43 | let attributes: UICollectionViewLayoutAttributes? = super
44 | .finalLayoutAttributesForDisappearingItem(at: itemIndexPath)
45 | attributes?.alpha = 1.0
46 | return attributes
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/Components/Protocols/StaticTextComponentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 01.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoEquatableComponent,AutoHashableComponent
11 | // sourcery: Component = StaticTextComponent,isFrameworkComponent
12 | /// StaticTextComponentType is a protocol for Component data structures that represent static text.
13 | public protocol StaticTextComponentType: Component {
14 |
15 | // sourcery: defaultValue = "Text.unattributed("")"
16 | /// The text to be displayed on the Component as either a String or NSAttributedString. The default
17 | /// value is Text.unattributed("").
18 | var text: Text { get set }
19 |
20 | // sourcery: defaultValue = "UIFont.systemFont(ofSize: 13.0)"
21 | /// The font of the Text. The default value is UIFont.systemFont(ofSize: 13.0).
22 | var font: UIFont { get set }
23 |
24 | // sourcery: defaultValue = UIColor.clear
25 | /// The backgroundColor for the entire content. The default value is UIColor.clear.
26 | var backgroundColor: UIColor { get set }
27 |
28 | // sourcery: defaultValue = "0.0"
29 | /// The lineFragmentPadding for the UITextView. The default value is 0.0.
30 | var lineFragmentPadding: CGFloat { get set }
31 |
32 | // sourcery: defaultValue = UIEdgeInsets.zero
33 | // sourcery: skipHashing, skipEquality
34 | /// The insets for the textContainerInset in the underlying TextViewLayout. Default value is 0.0.
35 | var insets: UIEdgeInsets { get set }
36 |
37 | // sourcery: defaultValue = Alignment.centerLeading
38 | // sourcery: skipHashing, skipEquality
39 | /// The alignment for the underlying TextViewLayout. The default value is Alignment.centerLeading.
40 | var alignment: Alignment { get set }
41 |
42 | // sourcery: defaultValue = TextViewLayoutDefaults.defaultFlexibility
43 | // sourcery: skipHashing, skipEquality
44 | /// The flexibility of the underlying TextViewLayout. The default value is TextViewLayoutDefaults.defaultFlexibility.
45 | var flexibility: Flexibility { get set }
46 |
47 | // sourcery: defaultValue = "{ _ in }"
48 | // sourcery: skipHashing, skipEquality
49 | /// The configuration applied to the UITextView. The default closure does nothing.
50 | var configuration: (UITextView) -> Void { get set }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/Enums/Async.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 13.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | /**
8 | Represents some data/model that is must be retrieved some time after the screen in rendered.
9 | */
10 | public enum Async: Hashable {
11 |
12 | // MARK: Static Methods
13 | public static func == (lhs: Async, rhs: Async) -> Bool {
14 | switch (lhs, rhs) {
15 | case let (.success(lhsValue), .success(rhsValue)):
16 | return lhsValue == rhsValue
17 |
18 | case let (.failure(lhsError), .failure(rhsError)):
19 | return type(of: lhsError) == type(of: rhsError) &&
20 | lhsError.errorDescription == rhsError.errorDescription
21 |
22 | case (.loading, .loading):
23 | return true
24 |
25 | case (.uninitialized, .uninitialized):
26 | return true
27 |
28 | default:
29 | return false
30 | }
31 | }
32 |
33 | /**
34 | The data/model was successfully fetched.
35 | */
36 | case success(T)
37 |
38 | /**
39 | An error was encountered while trying to fetched the data/model.
40 | */
41 | case failure(CyanicError)
42 |
43 | /**
44 | The data/model is being fetched.
45 | */
46 | case loading
47 |
48 | /**
49 | The data/model has not been fetched and the process to get it has not started
50 | */
51 | case uninitialized
52 |
53 | // MARK: Computed Properties
54 | /**
55 | Returns the underlying model if this instance is a .success case. Otherwise, returns nil.
56 | */
57 | public var value: T? {
58 | guard case let .success(model) = self else { return nil }
59 | return model
60 | }
61 |
62 | // MARK: Instance Methods
63 | public func hash(into hasher: inout Hasher) {
64 | switch self {
65 | case .success(let value):
66 | value.hash(into: &hasher)
67 |
68 | case .failure(let error):
69 | error.localizedDescription.hash(into: &hasher)
70 | ObjectIdentifier(type(of: self)).hash(into: &hasher)
71 |
72 | case .loading:
73 | "loading".hash(into: &hasher)
74 |
75 | case .uninitialized:
76 | "uninitialized".hash(into: &hasher)
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/Components/ExpandableComponent/ExpandableComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 15.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension,RequiredVariables
10 | // sourcery: ComponentLayout = ExpandableComponentLayout
11 | /// An ExpandableComponent is a Component that represents an expandable UI element that shows/hides other UI elements
12 | /// grouped with it.
13 | public struct ExpandableComponent: ExpandableComponentType, Selectable {
14 |
15 | // sourcery:inline:auto:ExpandableComponent.AutoGenerateComponent
16 | /**
17 | Work around Initializer because memberwise initializers are all or nothing
18 | - Parameters:
19 | - id: The unique identifier of the ExpandableComponent.
20 | */
21 | public init(id: String) {
22 | self.id = id
23 | }
24 |
25 | public var id: String
26 |
27 | public var width: CGFloat = 0.0
28 |
29 | public var contentLayout: ExpandableContentLayout = EmptyContentLayout()
30 |
31 | public var backgroundColor: UIColor = UIColor.clear
32 |
33 | // sourcery: skipHashing, skipEquality
34 | public var insets: UIEdgeInsets = UIEdgeInsets.zero
35 |
36 | // sourcery: skipHashing, skipEquality
37 | public var accessoryViewType: UIView.Type = UIView.self
38 |
39 | public var accessoryViewSize: CGSize = CGSize(width: 12.0, height: 12.0)
40 |
41 | // sourcery: skipHashing, skipEquality
42 | public var accessoryViewConfiguration: (UIView) -> Void = { _ in }
43 |
44 | // sourcery: skipHashing, skipEquality
45 | public var configuration: (UIView) -> Void = { _ in }
46 |
47 | public var isExpanded: Bool = false
48 |
49 | // sourcery: skipHashing, skipEquality
50 | public var setExpandableState: (String, Bool) -> Void = { (_: String, _: Bool) -> Void in
51 | fatalError("This default closure must be replaced!")
52 | }
53 |
54 | // sourcery: skipHashing, skipEquality
55 | public var dividerLine: DividerLine?
56 |
57 | public var height: CGFloat = 44.0
58 |
59 | // sourcery: skipHashing, skipEquality
60 | public var layout: ComponentLayout { return ExpandableComponentLayout(component: self) }
61 |
62 | public var identity: ExpandableComponent { return self }
63 | // sourcery:end
64 |
65 | public func onSelect(_ view: UIView) {
66 | self.setExpandableState(self.id, !self.isExpanded)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/Enums/ComponentStateValidator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 13.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | public enum ComponentStateValidator {
10 |
11 | /**
12 | Checks if the Component instance has a valid identifier.
13 | - Parameters:
14 | - component: The Component instance to be checked.
15 | - Returns:
16 | Bool indicating whether or not the id is valid.
17 | */
18 | public static func hasValidIdentifier(_ component: ConcreteComponent) -> Bool {
19 | return component.id != Constants.invalidID
20 | }
21 |
22 | /**
23 | Checks if the ExpandableComponent instance has a valid contentLayout
24 | - Parameters:
25 | - component: The ExpandableComponent instance to be checked.
26 | - Returns:
27 | Bool indicating whether or not the component's state is valid.
28 | */
29 | public static func validateExpandableComponent(_ component: ExpandableComponent) -> Bool {
30 | let isValidContentLayout: Bool = !(component.contentLayout is EmptyContentLayout)
31 | var validations: [Bool] = [isValidContentLayout]
32 | if let contentLayout = component.contentLayout as? ImageLabelContentLayout {
33 | let size: CGSize = contentLayout.imageSize
34 | let isValid: Bool = !(size.width == 0.0 || size.height == 0.0)
35 | #if DEBUG
36 | if !isValid {
37 | let errorString: String = "Your imageSize cannot have zero values"
38 | print("ExpandableError: \(errorString)")
39 | }
40 | #endif
41 | validations.append(isValid)
42 | }
43 |
44 | return !validations.contains(false)
45 |
46 | }
47 |
48 | /**
49 | Checks if the ChildVCComponent instance has a valid contentLayout
50 | - Parameters:
51 | - component: The ChildVCComponent instance to be checked.
52 | - Returns:
53 | Bool indicating whether or not the component's state is valid.
54 | */
55 | public static func validateChildVCComponent(_ component: ChildVCComponent) -> Bool {
56 | var component: ChildVCComponent = component
57 | let childVC: UIViewController & CyanicChildVCType = component.childVC
58 | let isInvalidChildComponentVC: Bool = childVC is InvalidChildComponentVC
59 | return !isInvalidChildComponentVC && component.parentVC != nil
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/Components/ChildVCComponent/ChildVCComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 07.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | /**
11 | The ChildVCComponentLayout is a ComponentLayout that is a subclass of SizeLayout.
12 | Used to size the view property of the childVC
13 | */
14 | public final class ChildVCComponentLayout: SizeLayout, ComponentLayout {
15 |
16 | /**
17 | Initializer.
18 | - Parameters:
19 | - component: The ChildVCComponent whose properties define the UI characters of the subviews to be created.
20 | */
21 | public init(component: ChildVCComponent) {
22 | var component: ChildVCComponent = component
23 | let size: CGSize = component.size
24 | let insets: UIEdgeInsets = component.insets
25 | let childVC: UIViewController & CyanicChildVCType = component.childVC
26 | let parentVC: UIViewController? = component.parentVC
27 |
28 | let sizeLayout: SizeLayout = SizeLayout(
29 | size: size,
30 | viewReuseId: "\(ChildVCComponentLayout.identifier)ChildVCView",
31 | sublayout: nil,
32 | config: { [weak childVC, weak parentVC] (view: UIView) -> Void in
33 | guard let childVC = childVC, let parentVC = parentVC else { return }
34 |
35 | if childVC.parent !== parentVC {
36 | parentVC.addChild(childVC)
37 | childVC.view.frame = view.bounds
38 | view.addSubview(childVC.view)
39 | childVC.didMove(toParent: parentVC)
40 | } else {
41 | childVC.view.frame = view.bounds
42 | view.addSubview(childVC.view)
43 | }
44 |
45 | component.configuration(childVC)
46 | }
47 | )
48 |
49 | let insetLayout: InsetLayout = InsetLayout(
50 | insets: insets,
51 | sublayout: sizeLayout
52 | )
53 |
54 | super.init(
55 | minWidth: size.width,
56 | maxWidth: size.width,
57 | minHeight: size.height + insets.top + insets.bottom,
58 | maxHeight: size.height + insets.top + insets.bottom,
59 | viewReuseId: "\(ChildVCComponentLayout.identifier)Size",
60 | sublayout: insetLayout,
61 | config: nil
62 | )
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/Components/Protocols/StaticLabelComponentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 25.08.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoEquatableComponent,AutoHashableComponent,
11 | // sourcery: Component = StaticLabelComponent,isFrameworkComponent
12 | /// A StaticLabelComponentType is a protocol adopted by Components that represent a UICollectionViewCell/UITableViewCell that
13 | /// displays static text on a UILabel in its contentView.
14 | public protocol StaticLabelComponentType: Component {
15 |
16 | // sourcery: defaultValue = "Text.unattributed("")"
17 | /// The text to be displayed on the Component as either a String or NSAttributedString. The default
18 | /// value is Text.unattributed("").
19 | var text: Text { get set }
20 |
21 | // sourcery: defaultValue = "UIFont.systemFont(ofSize: 13.0)"
22 | /// The font of the Text. The default value is UIFont.systemFont(ofSize: 13.0).
23 | var font: UIFont { get set }
24 |
25 | // sourcery: defaultValue = UIColor.clear
26 | /// The backgroundColor for the entire content. The default value is UIColor.clear.
27 | var backgroundColor: UIColor { get set }
28 |
29 | // sourcery: defaultValue = "0"
30 | /// The numberOfLines for the UILabel. The default value is 0.
31 | var numberOfLines: Int { get set }
32 |
33 | // sourcery: defaultValue = "NSLineBreakMode.byTruncatingTail"
34 | /// The lineBreakMode for the UILabel. The default value is NSLineBreakMode.byTruncatingTail.
35 | var lineBreakMode: NSLineBreakMode { get set }
36 |
37 | // sourcery: defaultValue = UIEdgeInsets.zero
38 | // sourcery: skipHashing, skipEquality
39 | /// The insets for the content. Default value is 0.0.
40 | var insets: UIEdgeInsets { get set }
41 |
42 | // sourcery: defaultValue = Alignment.centerLeading
43 | // sourcery: skipHashing, skipEquality
44 | /// The alignment for the underlying LabelLayout. The default value is Alignment.centerLeading.
45 | var alignment: Alignment { get set }
46 |
47 | // sourcery: defaultValue = "Flexibility.flexible"
48 | // sourcery: skipHashing, skipEquality
49 | /// The flexibility of the underlying LabelLayout. The default value is Flexible.flexible.
50 | var flexibility: Flexibility { get set }
51 |
52 | // sourcery: defaultValue = "{ _ in }"
53 | // sourcery: skipHashing, skipEquality
54 | /// The configuration applied to the UILabel. The default closure does nothing.
55 | var configuration: (UILabel) -> Void { get set }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 2/7/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Cyanic
11 | import RxSwift
12 | import SideMenu
13 |
14 | @UIApplicationMain
15 | class AppDelegate: UIResponder, UIApplicationDelegate {
16 |
17 | var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
18 |
19 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
20 | let vc: ExampleLoginVC = ExampleLoginVC(
21 | viewModelOne: UsernameViewModel(initialState: UsernameState.default),
22 | viewModelTwo: PasswordViewModel(initialState: PasswordState.default)
23 | )
24 |
25 | let nvc: UINavigationController = UINavigationController(rootViewController: vc)
26 |
27 | self.window?.rootViewController = nvc
28 | self.window?.makeKeyAndVisible()
29 |
30 | let rightVC: SideMenuNavigationController = SideMenuNavigationController(
31 | rootViewController: ExampleCounterVC()
32 | )
33 | SideMenuManager.default.rightMenuNavigationController = rightVC
34 | rightVC.sideMenuDelegate = self
35 | rightVC.enableSwipeToDismissGesture = false
36 | rightVC.presentationStyle = .menuSlideIn
37 | rightVC.presentationStyle.onTopShadowRadius = 0.0
38 | rightVC.presentationStyle.onTopShadowColor = UIColor.clear
39 | rightVC.presentationStyle.onTopShadowOpacity = 0.0
40 | rightVC.dismissOnPush = false
41 | rightVC.statusBarEndAlpha = 0.0
42 |
43 | // _ = Observable.interval(.seconds(1), scheduler: MainScheduler.instance)
44 | // .subscribe(onNext: { _ in
45 | // print("Resource count \(RxSwift.Resources.total)")
46 | // })
47 |
48 | return true
49 | }
50 |
51 | }
52 |
53 | extension AppDelegate: SideMenuNavigationControllerDelegate {
54 | func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) {
55 | print("Side Menu Will Appear")
56 | }
57 | func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) {
58 | print("Side Menu Did Appear")
59 | }
60 | func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) {
61 | print("Side Menu Will Disappear")
62 | }
63 | func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) {
64 | print("Side Menu Did Disappear")
65 | }
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/Tests/CyanicViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CyanicViewController.swift
3 | // Tests
4 | //
5 | // Created by Julio Miguel Alorro on 3/24/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | @testable import Cyanic
12 |
13 | class CyanicViewControllerTests: QuickSpec {
14 |
15 | override func spec() {
16 | describe("BaseStateListeningVC functionality") {
17 | context("When BaseStateListeningVC is initialized and view is loaded") {
18 | let vc: TestVC = TestVC()
19 | vc.viewDidLoad()
20 | it("should call invalidate to get the intial state") {
21 | expect(vc.count).toEventually(equal(1))
22 | }
23 |
24 | it("should call invalidate when any viewModel's state changes") {
25 | vc.viewModelOne.setState(with: { $0.changeCount += 1 })
26 | expect(vc.count).toEventually(equal(2))
27 | expect(vc.viewModelOne.currentState.changeCount).toEventually(equal(1))
28 |
29 | vc.viewModelTwo.setState(with: { $0.changeCount += 1})
30 | expect(vc.count).toEventually(equal(3))
31 | expect(vc.viewModelTwo.currentState.changeCount).toEventually(equal(1))
32 | }
33 | }
34 | }
35 | }
36 |
37 | class TestVC: CyanicViewController {
38 |
39 | var count: Int = 0
40 | let viewModelOne: TestViewModel1 = TestViewModel1(initialState: TestVC.TestState1.default)
41 | let viewModelTwo: TestViewModel2 = TestViewModel2(initialState: TestVC.TestState2.default)
42 |
43 | override var viewModels: [AnyViewModel] {
44 | return [
45 | self.viewModelOne.asAnyViewModel,
46 | self.viewModelTwo.asAnyViewModel
47 | ]
48 | }
49 |
50 | override func invalidate() {
51 | self.count += 1
52 | }
53 |
54 | class TestViewModel1: ViewModel {}
55 |
56 | struct TestState1: State {
57 |
58 | static var `default`: TestState1 {
59 | return TestState1(changeCount: 0)
60 | }
61 |
62 | var changeCount: Int
63 | }
64 |
65 | class TestViewModel2: ViewModel {}
66 |
67 | struct TestState2: State {
68 |
69 | static var `default`: TestState2 {
70 | return TestState2(changeCount: 0)
71 | }
72 |
73 | var changeCount: Int
74 | }
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/Protocols/Component.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 07.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import CoreGraphics
8 | import RxSwift
9 | import Differentiator
10 |
11 | /**
12 | Component is the data model representation of the UICollectionViewCell/UITableViewCell rendered on a ComponentViewController.
13 | A Component should be an immutable struct and it should contain UI specific characteristics related to the content
14 | that should be displayed in a CollectionComponentCell/TableComponentCell.
15 |
16 | A Component is what is used to diff between two collections by RxDataSources.
17 | */
18 | public protocol Component: IdentifiableType, Copyable, UserInterfaceModel, CustomStringConvertible where Identity == Self {
19 |
20 | /// The unique id of the Component. This is mutable because structs are the only data structure
21 | /// that should conform to Component this allows deep copying of Component (assuming all the other properties are
22 | /// value types) via the Copyable protocol's copy method.
23 | var id: String { get set }
24 |
25 | // sourcery: defaultValue = "0.0"
26 | /// The width of the UICollectionViewCell/UITableViewCell that hosts the content created by the Component.
27 | /// This should not be modified because it will be set by the framework. Mutating this won't do anything.
28 | var width: CGFloat { get set }
29 |
30 | }
31 |
32 | public extension Component {
33 |
34 | // sourcery: isExcluded
35 | // sourcery: skipHashing, skipEquality
36 | /// Since Component has a generic constraint. AnyComponent is used as a type erased wrapper around it
37 | /// so Components can be grouped in Collections.
38 | var asAnyComponent: AnyComponent {
39 | return AnyComponent(self)
40 | }
41 |
42 | }
43 |
44 | extension Component {
45 |
46 | // sourcery: isExcluded
47 | // sourcery: skipHashing, skipEquality
48 | public var description: String {
49 | return self.id
50 | }
51 |
52 | }
53 |
54 | public protocol StaticHeightComponent: Component {
55 |
56 | // sourcery: defaultValue = "44.0"
57 | /// The height of the UICollectionViewCell/UITableViewCell that hosts the content created by the Component.
58 | /// The default value is 44.0
59 | var height: CGFloat { get set }
60 |
61 | }
62 |
63 | public extension StaticHeightComponent {
64 |
65 | // sourcery: isExcluded
66 | // sourcery: skipHashing, skipEquality
67 | /// The size of the StaticHeightComponent.
68 | var size: CGSize {
69 | return CGSize(width: self.width, height: self.height)
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/Views/ComponentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 07.08.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import RxCocoa
9 | import RxSwift
10 | import UIKit
11 |
12 | /**
13 | ComponentView is a UIView that acts as the root UIView for Cyanic Components.
14 | */
15 | open class ComponentView: UIView {
16 |
17 | // MARK: Stored Properties
18 | /**
19 | The Component that arranges the subviews within this UIView instance.
20 | */
21 | open var component: AnyComponent? {
22 | didSet {
23 | guard let component = self.component else { return }
24 | self.configure(with: component)
25 | }
26 | }
27 |
28 | public private(set) var layout: ComponentLayout?
29 | private lazy var tap: UITapGestureRecognizer = UITapGestureRecognizer()
30 | private lazy var disposeBag: DisposeBag = DisposeBag()
31 | private lazy var disposable: SerialDisposable = {
32 | let d: SerialDisposable = SerialDisposable()
33 | d.disposed(by: self.disposeBag)
34 | return d
35 | }()
36 |
37 | // MARK: Methods
38 | open func configure(with component: AnyComponent) {
39 | self.layout = self.component?.layout
40 | self.frame.size = self.intrinsicContentSize
41 |
42 | self.layout?.arrangement(
43 | origin: self.bounds.origin,
44 | width: self.bounds.size.width,
45 | height: self.bounds.size.height
46 | )
47 | .makeViews(in: self)
48 |
49 | if let component = self.component?.identity.base as? Selectable {
50 | self.disposable.disposable = self.tap.rx.event.subscribe(
51 | onNext: { [weak self] (_: UITapGestureRecognizer) -> Void in
52 | guard let s = self else { return }
53 | component.onSelect(s)
54 | },
55 | onDisposed: { [weak self] () -> Void in
56 | guard let s = self else { return }
57 | s.removeGestureRecognizer(s.tap)
58 | }
59 | )
60 |
61 | self.addGestureRecognizer(self.tap)
62 | }
63 | }
64 |
65 | open override func sizeThatFits(_ size: CGSize) -> CGSize {
66 | guard let size = self.layout?.measurement(within: size).size else { return CGSize.zero }
67 | return size
68 | }
69 |
70 | open override var intrinsicContentSize: CGSize {
71 | return self.sizeThatFits(
72 | CGSize(
73 | width: self.bounds.width,
74 | height: CGFloat.greatestFiniteMagnitude
75 | )
76 | )
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/Components/Protocols/ButtonComponentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 27.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoEquatableComponent,AutoHashableComponent
11 | // sourcery: Component = ButtonComponent,isFrameworkComponent
12 | /// ButtonComponentType is a protocol for Components that represents a UIButton.
13 | public protocol ButtonComponentType: StaticHeightComponent {
14 |
15 | // sourcery: defaultValue = ButtonLayoutType.system
16 | /// The ButtonLayoutType which maps to UIButton.ButtonType. The default value is .system.
17 | var type: ButtonLayoutType { get set }
18 |
19 | // sourcery: defaultValue = """"
20 | /// The title displayed as text on the UIButton. The default value is an empty string: "".
21 | var title: String { get set }
22 |
23 | // sourcery: defaultValue = "UIFont.systemFont(ofSize: 15.0)"
24 | /// The title displayed as text on the UIButton. The default value is an empty string: "".
25 | var font: UIFont { get set }
26 |
27 | // sourcery: defaultValue = UIEdgeInsets.zero
28 | // sourcery: skipHashing, skipEquality
29 | /// The insets on the UIButton relative to its root UIView. This is NOT the insets on the content inside the
30 | /// UIButton. The default value is UIEdgeInsets.zero.
31 | var insets: UIEdgeInsets { get set }
32 |
33 | // sourcery: defaultValue = UIColor.clear
34 | /// The background color of the UICollectionView's contentView. The default value is UIColor.clear.
35 | var backgroundColor: UIColor { get set }
36 |
37 | // sourcery: defaultValue = ButtonLayoutDefaults.defaultAlignment
38 | // sourcery: skipHashing, skipEquality
39 | /// The alignment of the underlying ButtonLayout and SizeLayout. The default value is
40 | /// ButtonLayoutDefaults.defaultAlignment.
41 | var alignment: Alignment { get set }
42 |
43 | // sourcery: defaultValue = ButtonLayoutDefaults.defaultFlexibility
44 | // sourcery: skipHashing, skipEquality
45 | /// The flexibility of the underlying ButotnLayout and SizeLayout. The default value is
46 | /// ButtonLayoutDefaults.defaultFlexibility.
47 | var flexibility: Flexibility { get set }
48 |
49 | // sourcery: defaultValue = "{ _ in }"
50 | // sourcery: skipHashing, skipEquality
51 | /// The styling applied to the UIButton. The default value is an empty style.
52 | var configuration: (UIButton) -> Void { get set }
53 |
54 | // sourcery: defaultValue = { _ in print("Hello World \(#file)") }
55 | // sourcery: skipHashing, skipEquality
56 | /// The code executed when the UIButton is tapped.
57 | var onTap: (UIButton) -> Void { get set }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Utilities/ComponentsController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 18.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import CoreGraphics
8 |
9 | /**
10 | ComponentsController is responsible for managing an Array of AnyComponents. It functions as the data source for models
11 | to display on a UICollectionView/UITableView.
12 | */
13 | public struct ComponentsController {
14 |
15 | /**
16 | Initializer.
17 | - Parameters:
18 | - width: The width of the UICollectionViewCell/UITableViewCell. ComponentsController mutates the width property of every
19 | Component that is added to its Array.
20 | */
21 | public init(width: CGFloat) {
22 | self.width = width
23 | }
24 |
25 | /**
26 | The width of the UICollectionView/UITableViewCell where the Components will be displayed.
27 | */
28 | public let width: CGFloat
29 |
30 | /**
31 | The total height of the Components will be displayed.
32 | */
33 | public var height: CGFloat {
34 | return self.components.reduce(into: 0.0, { (currentHeight: inout CGFloat, component: AnyComponent) -> Void in
35 | currentHeight += component.layout.measurement(
36 | within: CGSize(width: self.width, height: CGFloat.greatestFiniteMagnitude)
37 | )
38 | .size
39 | .height
40 | })
41 | }
42 |
43 | /**
44 | The Array of AnyComponents managed by this ComponentsController.
45 | */
46 | public internal(set) var components: [AnyComponent] = []
47 |
48 | /**
49 | Adds a Component to the array as an AnyComponent instance.
50 | - Parameters:
51 | - component: The Component instance to be added to the components array.
52 | */
53 | public mutating func add(_ component: C) {
54 | self.components.append(component.asAnyComponent)
55 | }
56 |
57 | /**
58 | Adds Components of the same type to the array.
59 | - Parameters:
60 | - components: The Component instances to be added to the components array.
61 | */
62 | public mutating func add(_ components: Components) where Components.Element == C {
63 | self.components.append(contentsOf: components.map({ $0.asAnyComponent}))
64 | }
65 |
66 | /**
67 | Adds Components of the same type to the array. Variadic version of the method.
68 | - Parameters:
69 | - components: The Component instances to be added to the components array.
70 | */
71 | public mutating func add(_ components: Components...) {
72 | self.components.append(contentsOf: components.map({ $0.asAnyComponent}))
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/Components/ButtonComponent/ButtonComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 10.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import RxSwift
9 | import UIKit
10 |
11 | /**
12 | The ButtonComponentLayout is a ComponentLayout that is a subclass of SizeLayout.
13 | Used to create, size, and arrange the subviews associated with ButtonComponent.
14 | */
15 | open class ButtonComponentLayout: SizeLayout, ComponentLayout {
16 |
17 | /**
18 | Initializer.
19 | - Parameters:
20 | - component: The ButtonComponent whose properties define the UI characters of the subviews to be created.
21 | */
22 | public init(component: ButtonComponent) {
23 | let size: CGSize = component.size
24 | let insets: UIEdgeInsets = component.insets
25 | let adjustedHeight: CGFloat = size.height + insets.top + insets.bottom
26 |
27 | let serialDisposable: SerialDisposable = SerialDisposable()
28 | let disposeBag: DisposeBag = DisposeBag()
29 | serialDisposable.disposed(by: disposeBag)
30 | self.disposeBag = disposeBag
31 |
32 | let buttonLayout: ButtonLayout = ButtonLayout(
33 | type: component.type,
34 | title: component.title,
35 | image: ButtonLayoutImage.size(CGSize(width: size.width, height: adjustedHeight)),
36 | font: component.font,
37 | alignment: component.alignment,
38 | flexibility: component.flexibility,
39 | config: { (view: UIButton) -> Void in
40 | component.configuration(view)
41 | serialDisposable.disposable = view.rx.controlEvent(UIControl.Event.touchUpInside)
42 | .map { () -> UIButton in
43 | return view
44 | }
45 | .bind(onNext: component.onTap)
46 | }
47 | )
48 |
49 | let insetLayout: InsetLayout = InsetLayout(
50 | insets: component.insets,
51 | flexibility: Flexibility.inflexible,
52 | viewReuseId: "\(ButtonComponentLayout.identifier)InsetLayout",
53 | sublayout: buttonLayout
54 | )
55 |
56 | super.init(
57 | minWidth: size.width,
58 | maxWidth: size.width,
59 | minHeight: size.height + insets.top + insets.bottom,
60 | maxHeight: size.height + insets.top + insets.bottom,
61 | alignment: component.alignment,
62 | flexibility: component.flexibility,
63 | viewReuseId: "\(ButtonComponentLayout.identifier)SizeLayout",
64 | sublayout: insetLayout,
65 | config: { (view: UIView) -> Void in
66 | view.backgroundColor = component.backgroundColor
67 | }
68 | )
69 | }
70 |
71 | public let disposeBag: DisposeBag
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/Components/Protocols/ExpandableComponentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 01.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | // sourcery: AutoEquatableComponent,AutoHashableComponent
10 | // sourcery: Component = ExpandableComponent,isFrameworkComponent
11 | /// ExpandableComponentType is a protocol for Component data structures that want to function like section headers
12 | /// with content that can be hidden / shown on tap.
13 | public protocol ExpandableComponentType: StaticHeightComponent {
14 |
15 | // sourcery: defaultValue = "EmptyContentLayout()"
16 | /// The content of the ExpandableComponentType to be displayed. Excludes the accessory UIView.
17 | var contentLayout: ExpandableContentLayout { get set }
18 |
19 | // sourcery: defaultValue = UIColor.clear
20 | /// The backgroundColor for the entire content of the ExpandableComponentType. The default value is UIColor.clear.
21 | var backgroundColor: UIColor { get set }
22 |
23 | // sourcery: defaultValue = UIEdgeInsets.zero
24 | // sourcery: skipHashing, skipEquality
25 | /// The insets for the entire content of the ExpandableComponentType including the accessory UIView. The default value is UIEdgeInsets.zero.
26 | var insets: UIEdgeInsets { get set }
27 |
28 | // sourcery: defaultValue = "UIView.self"
29 | // sourcery: skipHashing, skipEquality
30 | /// The UIView type used as the accessory UIView instance. The default value is UIView.self.
31 | var accessoryViewType: UIView.Type { get set }
32 |
33 | // sourcery: defaultValue = "CGSize(width: 12.0, height: 12.0)"
34 | /// The size of the accessory UIView instance. The default value is CGSize(width: 12.0, height: 12.0).
35 | var accessoryViewSize: CGSize { get set }
36 |
37 | // sourcery: defaultValue = "{ _ in }"
38 | // sourcery: skipHashing, skipEquality
39 | /// The configuration that will be applied accessory UIView instance. The default closure does nothing.
40 | var accessoryViewConfiguration: (UIView) -> Void { get set }
41 |
42 | // sourcery: defaultValue = "{ _ in }"
43 | // sourcery: skipHashing, skipEquality
44 | /// The configuration that will be applied to the root UIView. The default closure does nothing.
45 | var configuration: (UIView) -> Void { get set }
46 |
47 | // sourcery: defaultValue = false
48 | /// The state of the ExpandableComponentType that shows whether it is expanded or contracted.
49 | var isExpanded: Bool { get set }
50 |
51 | // sourcery: skipHashing,skipEquality
52 | // sourcery: defaultValue = "{ (_: String, _: Bool) -> Void in fatalError("This default closure must be replaced!") }"
53 | /// A reference to the function will set a new state when the ExpandableComponentType is tapped.
54 | var setExpandableState: (String, Bool) -> Void { get set }
55 |
56 | // sourcery: skipHashing,skipEquality
57 | /// The dividerLine
58 | var dividerLine: DividerLine? { get set }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/Components/TextFieldComponent/CyanicTextFieldDelegateProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 21.05.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | /**
10 | Serves as the UITextFieldDelegate for the UITextField created in the TextComponentLayout.
11 | */
12 | internal final class CyanicTextFieldDelegateProxy: NSObject {
13 |
14 | // MARK: Stored Properties
15 | /**
16 | The closure executed when the textFieldShouldBeginEditing delegate method is called.
17 | */
18 | internal var shouldBeginEditing: (UITextField) -> Bool = { _ in return true }
19 |
20 | /**
21 | The closure executed when the textFieldDidBeginEditing delegate method is called.
22 | */
23 | internal var didBeginEditing: (UITextField) -> Void = { _ in }
24 |
25 | /**
26 | The closure executed when the textFieldShouldEndEditing delegate method is called.
27 | */
28 | internal var shouldEndEditing: (UITextField) -> Bool = { _ in return true }
29 |
30 | /**
31 | The closure executed when the textFieldDidEndEditing delegate method is called.
32 | */
33 | internal var didEndEditing: (UITextField) -> Void = { _ in }
34 |
35 | /**
36 | The maximum number of characters allowed on the UITextField.
37 | */
38 | internal var maximumCharacterCount: Int = Int.max
39 |
40 | /**
41 | The closure executed when the textFieldShouldClear delegate method is called.
42 | */
43 | internal var shouldClear: (UITextField) -> Bool = { _ in return true }
44 |
45 | /**
46 | The closure executed when the textFieldShouldReturn delegate method is called.
47 | */
48 | internal var shouldReturn: (UITextField) -> Bool = { _ in return true }
49 | }
50 |
51 | extension CyanicTextFieldDelegateProxy: UITextFieldDelegate {
52 |
53 | internal func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
54 | return self.shouldBeginEditing(textField)
55 | }
56 |
57 | internal func textFieldDidBeginEditing(_ textField: UITextField) {
58 | self.didBeginEditing(textField)
59 | }
60 |
61 | internal func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
62 | return self.shouldEndEditing(textField)
63 | }
64 |
65 | internal func textFieldDidEndEditing(_ textField: UITextField) {
66 | self.didEndEditing(textField)
67 | }
68 |
69 | internal func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
70 | let currentText: String = textField.text ?? ""
71 | guard let stringRange = Range(range, in: currentText) else { return false }
72 | let updatedText: String = currentText.replacingCharacters(in: stringRange, with: string)
73 | return updatedText.count <= self.maximumCharacterCount
74 | }
75 |
76 | internal func textFieldShouldClear(_ textField: UITextField) -> Bool {
77 | return self.shouldClear(textField)
78 | }
79 |
80 | internal func textFieldShouldReturn(_ textField: UITextField) -> Bool {
81 | return self.shouldReturn(textField)
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/Components/TextViewComponent/TextViewComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 16.05.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 |
9 | /**
10 | The TextViewComponentLayout is a ComponentLayout that is a subclass of SizeLayout.
11 | Used to create, size, and arrange the subviews associated with TextViewComponent.
12 | */
13 | public final class TextViewComponentLayout: SizeLayout, ComponentLayout {
14 |
15 | /**
16 | Initializer.
17 | - Parameters:
18 | - component: The StaticTextComponent whose properties define the UI characters of the subviews to be created.
19 | */
20 | public init(component: TextViewComponent) {
21 |
22 | let textLayout: TextViewLayout = TextViewLayout(
23 | text: Text.unattributed(component.text),
24 | font: component.font,
25 | lineFragmentPadding: 0,
26 | textContainerInset: component.textContainerInset,
27 | layoutAlignment: component.alignment,
28 | flexibility: component.flexibility,
29 | viewReuseId: "\(TextViewComponentLayout.identifier)TextView",
30 | viewClass: component.textViewType,
31 | config: { (view: UITextView) -> Void in
32 | view.backgroundColor = component.backgroundColor
33 | view.isEditable = true
34 | view.isUserInteractionEnabled = true
35 | view.isScrollEnabled = true
36 | component.configuration(view)
37 |
38 | view.delegate = component.delegate
39 | guard let delegate = component.delegate as? CyanicTextViewDelegateProxy else { return }
40 | delegate.shouldBeginEditing = component.shouldBeginEditing
41 | delegate.didBeginEditing = component.didBeginEditing
42 | delegate.shouldEndEditing = component.shouldEndEditing
43 | delegate.didEndEditing = component.didEndEditing
44 | delegate.maximumCharacterCount = component.maximumCharacterCount
45 | delegate.didChange = component.didChange
46 | delegate.didChangeSelection = component.didChangeSelection
47 | delegate.shouldInteractWithURLInCharacterRange = component.shouldInteractWithURLInCharacterRange
48 | delegate
49 | .shouldInteractWithTextAttachmentInCharacterRange = component
50 | .shouldInteractWithTextAttachmentInCharacterRange
51 | }
52 | )
53 |
54 | let insetLayout: InsetLayout = InsetLayout(
55 | insets: component.insets,
56 | sublayout: textLayout
57 | )
58 |
59 | super.init(
60 | minWidth: component.width,
61 | maxWidth: component.width,
62 | minHeight: component.height,
63 | maxHeight: component.height,
64 | viewReuseId: "\(TextViewComponentLayout.identifier)Size",
65 | sublayout: insetLayout,
66 | config: {
67 | $0.backgroundColor = component.backgroundColor
68 | }
69 | )
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Example/ExampleCounter/ExampleCounterVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestVC.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 3/24/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Cyanic
11 | import RxCocoa
12 | import Kio
13 |
14 | class ExampleCounterVC: CyanicViewController {
15 |
16 | // MARK: UIViewController Lifecycle Methods
17 | override func loadView() {
18 | self.view = UIView()
19 | self.view.backgroundColor = UIColor(white: 0.85, alpha: 1.0)
20 |
21 | self.label = UILabel()
22 | self.label.textAlignment = .center
23 | self.label.text = self.count.description
24 |
25 | self.view.addSubview(self.label)
26 | self.label.translatesAutoresizingMaskIntoConstraints = false
27 |
28 | NSLayoutConstraint.activate([
29 | self.label.heightAnchor.constraint(equalToConstant: 50.0),
30 | self.label.widthAnchor.constraint(equalToConstant: 100.0),
31 | self.label.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
32 | self.label.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
33 | ])
34 |
35 | }
36 |
37 | override func viewDidLoad() {
38 | super.viewDidLoad()
39 |
40 | self.kio.setUpNavigationItem { (item: UINavigationItem) -> Void in
41 | item.rightBarButtonItem = UIBarButtonItem(
42 | barButtonSystemItem: UIBarButtonItem.SystemItem.add,
43 | target: self,
44 | action: #selector(ExampleCounterVC.addButtonItemTapped)
45 | )
46 | }
47 |
48 | // if self.presentingViewController != nil {
49 | let button: UIButton = UIButton(type: UIButton.ButtonType.system)
50 | button.setTitleColor(UIColor.black, for: UIControl.State.normal)
51 | button.setTitle("Back", for: UIControl.State.normal)
52 | button.addTarget(self, action: #selector(ExampleCounterVC.cancelButtonItemTapped), for: UIControl.Event.touchUpInside)
53 | button.frame = CGRect(origin: CGPoint(x: 100.0, y: 100.0), size: CGSize(width: 100.0, height: 44.0))
54 | button.translatesAutoresizingMaskIntoConstraints = false
55 | self.view.addSubview(button)
56 | self.view.bringSubviewToFront(button)
57 | // }
58 | }
59 |
60 | // MARK: Stored Properties
61 | private var label: UILabel!
62 | private var count: Int = 0
63 | private let viewModel: ExampleCounterViewModel = ExampleCounterViewModel(initialState: ExampleCounterState.default)
64 |
65 | // MARK: Computed Properties
66 | override var viewModels: [AnyViewModel] {
67 | return [
68 | self.viewModel.asAnyViewModel
69 | ]
70 | }
71 |
72 | // MARK: Methods
73 | override func invalidate() {
74 | self.label.text = self.count.description
75 | }
76 |
77 | // MARK: Target Action Methods
78 | @objc func addButtonItemTapped() {
79 | self.count += 1
80 | let count: Int = self.count
81 | self.viewModel.setState(with: { $0.changeCount = count })
82 | }
83 |
84 | @objc func cancelButtonItemTapped() {
85 | self.presentingViewController?.dismiss(animated: true, completion: nil)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/Components/TextFieldComponent/TextFieldComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 09.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // swiftlint:disable weak_delegate
11 |
12 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension
13 | // sourcery: ComponentLayout = TextFieldComponentLayout
14 | /// TextFieldComponent is a Component that represents a UITextField.
15 | /// - Note:
16 | /// When configuring the UITextfield, do not assign a UITextFieldDelegate because it will be overwritten.
17 | public struct TextFieldComponent: TextFieldComponentType {
18 |
19 | // sourcery:inline:auto:TextFieldComponent.AutoGenerateComponent
20 | /**
21 | Work around Initializer because memberwise initializers are all or nothing
22 | - Parameters:
23 | - id: The unique identifier of the TextFieldComponent.
24 | */
25 | public init(id: String) {
26 | self.id = id
27 | }
28 |
29 | public var id: String
30 |
31 | public var width: CGFloat = 0.0
32 |
33 | public var height: CGFloat = 44.0
34 |
35 | public var placeholder: String = ""
36 |
37 | // sourcery: skipHashing, skipEquality
38 | public var text: String = ""
39 |
40 | // sourcery: skipHashing, skipEquality
41 | public var insets: UIEdgeInsets = UIEdgeInsets.zero
42 |
43 | public var backgroundColor: UIColor = UIColor.clear
44 |
45 | // sourcery: skipHashing, skipEquality
46 | public var alignment: Alignment = Alignment.centerLeading
47 |
48 | // sourcery: skipHashing, skipEquality
49 | public var flexibility: Flexibility = Flexibility.flexible
50 |
51 | public var editingChangeDelay: Int = 500
52 |
53 | // sourcery: skipHashing, skipEquality
54 | public var configuration: (UITextField) -> Void = { _ in }
55 |
56 | // sourcery: skipHashing, skipEquality
57 | public var editingChanged: (UITextField) -> Void = { (_: UITextField) -> Void in print("TextField has new text") }
58 |
59 | // sourcery: skipHashing, skipEquality
60 | public var textFieldType: UITextField.Type = UITextField.self
61 |
62 | // sourcery: skipHashing, skipEquality
63 | public let delegate: UITextFieldDelegate = CyanicTextFieldDelegateProxy()
64 |
65 | // sourcery: skipHashing, skipEquality
66 | public var shouldBeginEditing: (UITextField) -> Bool = { _ in return true }
67 |
68 | // sourcery: skipHashing, skipEquality
69 | public var didBeginEditing: (UITextField) -> Void = { _ in }
70 |
71 | // sourcery: skipHashing, skipEquality
72 | public var shouldEndEditing: (UITextField) -> Bool = { _ in return true }
73 |
74 | // sourcery: skipHashing, skipEquality
75 | public var didEndEditing: (UITextField) -> Void = { _ in }
76 |
77 | public var maximumCharacterCount: Int = Int.max
78 |
79 | // sourcery: skipHashing, skipEquality
80 | public var shouldClear: (UITextField) -> Bool = { _ in return true }
81 |
82 | // sourcery: skipHashing, skipEquality
83 | public var shouldReturn: (UITextField) -> Bool = { _ in return true }
84 |
85 | // sourcery: skipHashing, skipEquality
86 | public var layout: ComponentLayout { return TextFieldComponentLayout(component: self) }
87 |
88 | public var identity: TextFieldComponent { return self }
89 | // sourcery:end
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/Components/TextViewComponent/TextViewComponent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 16.05.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // swiftlint:disable line_length weak_delegate
11 |
12 | // sourcery: AutoGenerateComponent,AutoGenerateComponentExtension
13 | // sourcery: ComponentLayout = TextViewComponentLayout
14 | public struct TextViewComponent: TextViewComponentType {
15 |
16 | // sourcery:inline:auto:TextViewComponent.AutoGenerateComponent
17 | /**
18 | Work around Initializer because memberwise initializers are all or nothing
19 | - Parameters:
20 | - id: The unique identifier of the TextViewComponent.
21 | */
22 | public init(id: String) {
23 | self.id = id
24 | }
25 |
26 | public var id: String
27 |
28 | public var width: CGFloat = 0.0
29 |
30 | public var height: CGFloat = 44.0
31 |
32 | // sourcery: skipHashing, skipEquality
33 | public var text: String = ""
34 |
35 | // sourcery: skipHashing, skipEquality
36 | public var font: UIFont = UIFont.systemFont(ofSize: 13.0)
37 |
38 | // sourcery: skipHashing, skipEquality
39 | public var insets: UIEdgeInsets = UIEdgeInsets.zero
40 |
41 | // sourcery: skipHashing, skipEquality
42 | public var textContainerInset: UIEdgeInsets = UIEdgeInsets.zero
43 |
44 | public var backgroundColor: UIColor = UIColor.clear
45 |
46 | // sourcery: skipHashing, skipEquality
47 | public var alignment: Alignment = Alignment.fill
48 |
49 | // sourcery: skipHashing, skipEquality
50 | public var flexibility: Flexibility = Flexibility.flexible
51 |
52 | // sourcery: skipHashing, skipEquality
53 | public var configuration: (UITextView) -> Void = { _ in }
54 |
55 | // sourcery: skipHashing, skipEquality
56 | public var textViewType: UITextView.Type = UITextView.self
57 |
58 | // sourcery: skipHashing, skipEquality
59 | public let delegate: UITextViewDelegate = CyanicTextViewDelegateProxy()
60 |
61 | // sourcery: skipHashing, skipEquality
62 | public var shouldBeginEditing: (UITextView) -> Bool = { _ in return true }
63 |
64 | // sourcery: skipHashing, skipEquality
65 | public var shouldEndEditing: (UITextView) -> Bool = { _ in return true }
66 |
67 | // sourcery: skipHashing, skipEquality
68 | public var didBeginEditing: (UITextView) -> Void = { _ in }
69 |
70 | // sourcery: skipHashing, skipEquality
71 | public var didEndEditing: (UITextView) -> Void = { _ in }
72 |
73 | // sourcery: skipHashing, skipEquality
74 | public var maximumCharacterCount: Int = Int.max
75 |
76 | // sourcery: skipHashing, skipEquality
77 | public var didChange: (UITextView) -> Void = { _ in }
78 |
79 | // sourcery: skipHashing, skipEquality
80 | public var didChangeSelection: (UITextView) -> Void = { _ in }
81 |
82 | // sourcery: skipHashing, skipEquality
83 | public var shouldInteractWithURLInCharacterRange: (UITextView, URL, NSRange, UITextItemInteraction) -> Bool = { _, _, _, _ in return true }
84 |
85 | // sourcery: skipHashing, skipEquality
86 | public var shouldInteractWithTextAttachmentInCharacterRange: (UITextView, NSTextAttachment, NSRange, UITextItemInteraction) -> Bool = { _, _, _, _ in return true }
87 |
88 | // sourcery: skipHashing, skipEquality
89 | public var layout: ComponentLayout { return TextViewComponentLayout(component: self) }
90 |
91 | public var identity: TextViewComponent { return self }
92 | // sourcery:end
93 | }
94 |
--------------------------------------------------------------------------------
/Sources/Components/TextFieldComponent/TextFieldComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 09.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 | import RxSwift
10 | import RxCocoa
11 |
12 | open class TextFieldComponentLayout: InsetLayout, ComponentLayout {
13 |
14 | public init(component: TextFieldComponent) { // swiftlint:disable:this function_body_length
15 | let insets: UIEdgeInsets = component.insets
16 |
17 | let serialDisposable: SerialDisposable = SerialDisposable()
18 | let disposeBag: DisposeBag = DisposeBag()
19 |
20 | serialDisposable.disposed(by: disposeBag)
21 |
22 | let size: CGSize = component.size
23 |
24 | let textFieldLayout: SizeLayout = SizeLayout(
25 | minWidth: size.width,
26 | maxWidth: size.width,
27 | minHeight: size.height,
28 | maxHeight: size.height,
29 | alignment: component.alignment,
30 | flexibility: component.flexibility,
31 | viewReuseId: "\(TextFieldComponentLayout.identifier)SizeLayout",
32 | viewClass: component.textFieldType,
33 | config: { (view: UIView) -> Void in
34 | guard let view = view as? UITextField else { return }
35 | component.configuration(view)
36 |
37 | view.text = component.text
38 | view.placeholder = component.placeholder
39 |
40 | let disposables: [Disposable] = [
41 | view.rx.controlEvent([UIControl.Event.editingChanged])
42 | .map({ () -> UITextField in
43 | return view
44 | })
45 | .debounce(RxTimeInterval.milliseconds(component.editingChangeDelay), scheduler: MainScheduler.instance)
46 | .bind(
47 | onNext: { (view: UITextField) -> Void in
48 | component.editingChanged(view)
49 | }
50 | )
51 | ]
52 |
53 | let compositeDisposable: CompositeDisposable = CompositeDisposable(disposables: disposables)
54 | serialDisposable.disposable = compositeDisposable
55 | view.delegate = component.delegate
56 |
57 | guard let delegate = component.delegate as? CyanicTextFieldDelegateProxy else { return }
58 | delegate.shouldBeginEditing = component.shouldBeginEditing
59 | delegate.didBeginEditing = component.didBeginEditing
60 | delegate.shouldEndEditing = component.shouldEndEditing
61 | delegate.didEndEditing = component.didEndEditing
62 | delegate.maximumCharacterCount = component.maximumCharacterCount
63 | delegate.shouldClear = component.shouldClear
64 | delegate.shouldReturn = component.shouldReturn
65 | }
66 | )
67 |
68 | self.disposeBag = disposeBag
69 | super.init(
70 | insets: insets,
71 | alignment: component.alignment,
72 | flexibility: component.flexibility,
73 | viewReuseId: "\(TextFieldComponentLayout.identifier)InsetLayout",
74 | sublayout: textFieldLayout,
75 | config: { (view: UIView) -> Void in
76 | view.backgroundColor = component.backgroundColor
77 | }
78 | )
79 | }
80 |
81 | public let disposeBag: DisposeBag
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/CollectionView/CollectionComponentViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 12.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | /**
10 | CollectionComponentViewController is a subclass of ComponentViewController. It serves as the base class for the
11 | SingleSectionCollectionComponentViewController and MultiSectionCollectionComponentViewController, therefore it contains
12 | the logic and implementations shared between the two subclasses.
13 | */
14 | open class CollectionComponentViewController: ComponentViewController, UICollectionViewDelegateFlowLayout {
15 |
16 | // MARK: UIViewController Lifecycle Methods
17 | open override func viewDidLoad() {
18 | super.viewDidLoad()
19 | self.collectionView.register(CollectionComponentCell.self, forCellWithReuseIdentifier: CollectionComponentCell.identifier)
20 |
21 | // Set up as the UICollectionView's UICollectionViewDelegateFlowLayout,
22 | // UICollectionViewDelegate, and UIScrollViewDelegate
23 | self.collectionView.delegate = self
24 | }
25 |
26 | open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
27 | super.viewWillTransition(to: size, with: coordinator)
28 | guard self._listView != nil else { return }
29 | self.collectionView.collectionViewLayout.invalidateLayout()
30 | }
31 |
32 | // MARK: Computed Properties
33 | /**
34 | The UICollectionView instance managed by this CollectionComponentViewController instance.
35 | */
36 | public var collectionView: UICollectionView {
37 | return self._listView as! UICollectionView // swiftlint:disable:this force_cast
38 | }
39 |
40 | internal typealias CombinedState = (CGSize, [Any])
41 |
42 | // MARK: Methods
43 | /**
44 | Creates a UICollectionView with a UICollectionViewLayout instantiated from the createUICollectionViewLayout method.
45 | This method is called in the ComponentViewController's loadView method.
46 | - Returns:
47 | - UICollectionView instance typed as a UIView.
48 | */
49 | open override func setUpListView() -> UIView {
50 | return UICollectionView(
51 | frame: CGRect.zero,
52 | collectionViewLayout: self.createUICollectionViewLayout()
53 | )
54 | }
55 |
56 | /**
57 | Creates the UICollectionViewLayout to be used by the UICollectionView managed by this CollectionComponentViewController.
58 | The default implementation creates a UICollectionViewFlowLayout with a minimumLineSpacing and minimumInteritemSpacing of
59 | 0.0.
60 | - Returns:
61 | A UICollectionViewLayout instance.
62 | */
63 | open func createUICollectionViewLayout() -> UICollectionViewLayout {
64 | let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
65 | layout.minimumLineSpacing = 0.0
66 | layout.minimumInteritemSpacing = 0.0
67 | return layout
68 | }
69 |
70 | // MARK: UICollectionViewDelegateFlowLayout Methods
71 | open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
72 | collectionView.deselectItem(at: indexPath, animated: false)
73 | guard let component: AnyComponent = self.component(at: indexPath) else { return }
74 | guard let selectable = component.identity.base as? Selectable else { return }
75 | guard let cell = collectionView.cellForItem(at: indexPath) else { return }
76 | selectable.onSelect(cell.contentView)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Cyanic.xcodeproj/xcshareddata/xcschemes/Example.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 |
--------------------------------------------------------------------------------
/Sources/TableView/TableComponentSectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 14.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import Foundation
8 | import LayoutKit
9 | import RxSwift
10 | import UIKit
11 |
12 | /**
13 | TableComponentSectionView serves as the root UIView for any section UIView for UITableViews.
14 | */
15 | public final class TableComponentSectionView: UIView {
16 |
17 | // MARK: Class Properties
18 | /**
19 | The String identifier used by the TableComponentSectionView.
20 | */
21 | public class var identifier: String {
22 | return String(describing: Mirror(reflecting: self).subjectType)
23 | }
24 |
25 | public override init(frame: CGRect) {
26 | super.init(frame: frame)
27 | self.serialDisposable.disposed(by: self.disposeBag)
28 | }
29 |
30 | public required init?(coder aDecoder: NSCoder) {
31 | fatalError("init(coder:) has not been implemented")
32 | }
33 |
34 | // MARK: Stored Properties
35 | /**
36 | The current ComponentLayout instance that creates and arranges the subviews in this TableComponentSectionView.
37 | */
38 | private var layout: ComponentLayout?
39 |
40 | /**
41 | The UITapGestureRecognizer instance that handles user tap gestures, if there is one.
42 | */
43 | private var tap: UITapGestureRecognizer?
44 |
45 | /**
46 | The DisposeBag that manages Rx-related subscriptions.
47 | */
48 | private let disposeBag: DisposeBag = DisposeBag()
49 |
50 | /**
51 | The SerialDisposable that ensures there is only one subscription at a time for the UITapGestureRecognizer.
52 | */
53 | private let serialDisposable: SerialDisposable = SerialDisposable()
54 |
55 | // MARK: Overridden Properties
56 | public override final var intrinsicContentSize: CGSize {
57 | return self.sizeThatFits(
58 | CGSize(
59 | width: Constants.screenWidth,
60 | height: CGFloat.greatestFiniteMagnitude
61 | )
62 | )
63 | }
64 |
65 | // MARK: Overridden Methods
66 | public override final func sizeThatFits(_ size: CGSize) -> CGSize {
67 | guard let size = self.layout?.measurement(within: size).size else { return CGSize.zero }
68 | return size
69 | }
70 |
71 | /**
72 | Reads the layout from the AnyComponent instance to create the subviews in this TableComponentSectionView instance. T
73 | his also sets the frame.size equal to its intrinsicContentSize and calls setNeedsLayout.
74 | - Parameters:
75 | - component: The AnyComponent instance that represents this TableComponentSectionView.
76 | */
77 | public func configure(with component: AnyComponent) {
78 | self.layout = component.layout
79 | self.frame.size = self.intrinsicContentSize
80 |
81 | if let selectable = component.identity.base as? Selectable {
82 | let tap: UITapGestureRecognizer = UITapGestureRecognizer()
83 |
84 | let disposable: Disposable = tap.rx.event.bind(onNext: { (_: UITapGestureRecognizer) -> Void in
85 | selectable.onSelect(self)
86 | })
87 |
88 | self.addGestureRecognizer(tap)
89 | self.serialDisposable.disposable = disposable
90 | self.tap = tap
91 | }
92 |
93 | self.layout?.arrangement(
94 | origin: self.bounds.origin,
95 | width: self.bounds.size.width,
96 | height: self.bounds.size.height
97 | )
98 | .makeViews(in: self)
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/Templates/AutoGenerateComponentExtensions.swifttemplate:
--------------------------------------------------------------------------------
1 | <%
2 | func lowerCamelCaseName(for type: Type) -> String {
3 | let name: String = type.name
4 | return name.prefix(1).lowercased() + name.dropFirst()
5 | }
6 | -%>
7 | // sourcery:file:DataSourceControllers+<%= argument["project"] as? String ?? "Cyanic" %>.swift
8 | <%_ if let isFramework = argument["isFramework"] as? Bool, isFramework {-%>
9 | <%_ } else { -%>
10 | import Cyanic
11 |
12 | <%_ } -%>
13 | // swiftlint:disable all
14 | public extension ComponentsController {
15 | <%_ for type in types.all where type.annotations["AutoGenerateComponentExtension"] != nil {-%>
16 | <%_ let functionName: String = lowerCamelCaseName(for: type) -%>
17 |
18 | /**
19 | Generates a <%= type.name %> instance and configures its properties with the given closure. You must provide a
20 | unique id in the configuration block, otherwise it will force a fatalError.
21 | - Parameters:
22 | - configuration: The closure that mutates the mutable <%= type.name %>.
23 | - mutableComponent: The <%= type.name %> instance to be mutated/configured.
24 | - Returns:
25 | <%= type.name %>
26 | */
27 | @discardableResult
28 | mutating func <%= functionName %>(configuration: (_ mutableComponent: inout <%= type.name %>) -> Void) -> <%= type.name %> {
29 | var mutableComponent: <%= type.name %> = <%= type.name %>(id: Constants.invalidID)
30 | configuration(&mutableComponent)
31 | mutableComponent.width = self.width
32 | guard ComponentStateValidator.hasValidIdentifier(mutableComponent)
33 | else { fatalError("You must have a unique identifier for this component") }
34 | <%_ if type.annotations["RequiredVariables"] != nil { -%>
35 | guard ComponentStateValidator.validate<%= type.name %>(mutableComponent)
36 | else { fatalError("You did not configure all required variables in this component") }
37 | <%_ } -%>
38 | self.add(mutableComponent)
39 | return mutableComponent
40 | }
41 | <%_ } -%>
42 | }
43 |
44 | public extension SectionController {
45 | <%_ for type in types.all where type.annotations["AutoGenerateComponentExtension"] != nil {-%>
46 | <%_ let functionName: String = lowerCamelCaseName(for: type) -%>
47 |
48 | /**
49 | Generates a <%= type.name %> instance and configures its properties with the given closure. You must provide a
50 | unique id in the configuration block, otherwise it will force a fatalError.
51 | - Parameters:
52 | - configuration: The closure that mutates the mutable <%= type.name %>.
53 | - mutableComponent: The <%= type.name %> instance to be mutated/configured.
54 | - Returns:
55 | <%= type.name %>
56 | */
57 | @discardableResult
58 | mutating func <%= functionName %>(for supplementaryView: SectionController.SupplementaryView, configuration: (_ mutableComponent: inout <%= type.name %>) -> Void) -> <%= type.name %> {
59 | var mutableComponent: <%= type.name %> = <%= type.name %>(id: Constants.invalidID)
60 | configuration(&mutableComponent)
61 | mutableComponent.width = self.width
62 | guard ComponentStateValidator.hasValidIdentifier(mutableComponent)
63 | else { fatalError("You must have a unique identifier for this component") }
64 | <%_ if type.annotations["RequiredVariables"] != nil { -%>
65 | guard ComponentStateValidator.validate<%= type.name %>(mutableComponent)
66 | else { fatalError("You did not configure all required variables in this component") }
67 | <%_ } -%>
68 |
69 | switch supplementaryView {
70 | case .header:
71 | self.headerComponent = mutableComponent.asAnyComponent
72 | case .footer:
73 | self.footerComponent = mutableComponent.asAnyComponent
74 | }
75 |
76 | return mutableComponent
77 | }
78 | <%_ } -%>
79 | }
80 | // swiftlint:enable all
81 | // sourcery:end
82 |
--------------------------------------------------------------------------------
/Sources/Sourcery/AutoHashableComponent+Cyanic.swift:
--------------------------------------------------------------------------------
1 | // Generated using Sourcery 0.17.0 — https://github.com/krzysztofzablocki/Sourcery
2 | // DO NOT EDIT
3 |
4 | // swiftlint:disable all
5 | // MARK: - AutoHashableComponent
6 | // MARK: - ButtonComponent AutoHashableComponent
7 | extension ButtonComponent: Hashable {
8 | public func hash(into hasher: inout Hasher) {
9 | self.type.hash(into: &hasher)
10 | self.title.hash(into: &hasher)
11 | self.font.hash(into: &hasher)
12 | self.backgroundColor.hash(into: &hasher)
13 | self.id.hash(into: &hasher)
14 | self.width.hash(into: &hasher)
15 | self.height.hash(into: &hasher)
16 | }
17 | }
18 | // MARK: - ChildVCComponent AutoHashableComponent
19 | extension ChildVCComponent: Hashable {
20 | public func hash(into hasher: inout Hasher) {
21 | self.name.hash(into: &hasher)
22 | self.description.hash(into: &hasher)
23 | self.id.hash(into: &hasher)
24 | self.width.hash(into: &hasher)
25 | self.height.hash(into: &hasher)
26 | }
27 | }
28 | // MARK: - ExpandableComponent AutoHashableComponent
29 | extension ExpandableComponent: Hashable {
30 | public func hash(into hasher: inout Hasher) {
31 | self.contentLayout.hash(into: &hasher)
32 | self.backgroundColor.hash(into: &hasher)
33 | self.accessoryViewSize.hash(into: &hasher)
34 | self.isExpanded.hash(into: &hasher)
35 | self.id.hash(into: &hasher)
36 | self.width.hash(into: &hasher)
37 | self.height.hash(into: &hasher)
38 | }
39 | }
40 | // MARK: - SizedComponent AutoHashableComponent
41 | extension SizedComponent: Hashable {
42 | public func hash(into hasher: inout Hasher) {
43 | self.backgroundColor.hash(into: &hasher)
44 | self.id.hash(into: &hasher)
45 | self.width.hash(into: &hasher)
46 | self.height.hash(into: &hasher)
47 | }
48 | }
49 | // MARK: - StaticLabelComponent AutoHashableComponent
50 | extension StaticLabelComponent: Hashable {
51 | public func hash(into hasher: inout Hasher) {
52 | self.text.hash(into: &hasher)
53 | self.font.hash(into: &hasher)
54 | self.backgroundColor.hash(into: &hasher)
55 | self.numberOfLines.hash(into: &hasher)
56 | self.lineBreakMode.hash(into: &hasher)
57 | self.id.hash(into: &hasher)
58 | self.width.hash(into: &hasher)
59 | }
60 | }
61 | // MARK: - StaticSpacingComponent AutoHashableComponent
62 | extension StaticSpacingComponent: Hashable {
63 | public func hash(into hasher: inout Hasher) {
64 | self.backgroundColor.hash(into: &hasher)
65 | self.id.hash(into: &hasher)
66 | self.width.hash(into: &hasher)
67 | self.height.hash(into: &hasher)
68 | }
69 | }
70 | // MARK: - StaticTextComponent AutoHashableComponent
71 | extension StaticTextComponent: Hashable {
72 | public func hash(into hasher: inout Hasher) {
73 | self.text.hash(into: &hasher)
74 | self.font.hash(into: &hasher)
75 | self.backgroundColor.hash(into: &hasher)
76 | self.lineFragmentPadding.hash(into: &hasher)
77 | self.id.hash(into: &hasher)
78 | self.width.hash(into: &hasher)
79 | }
80 | }
81 | // MARK: - TextFieldComponent AutoHashableComponent
82 | extension TextFieldComponent: Hashable {
83 | public func hash(into hasher: inout Hasher) {
84 | self.placeholder.hash(into: &hasher)
85 | self.backgroundColor.hash(into: &hasher)
86 | self.editingChangeDelay.hash(into: &hasher)
87 | self.maximumCharacterCount.hash(into: &hasher)
88 | self.id.hash(into: &hasher)
89 | self.width.hash(into: &hasher)
90 | self.height.hash(into: &hasher)
91 | }
92 | }
93 | // MARK: - TextViewComponent AutoHashableComponent
94 | extension TextViewComponent: Hashable {
95 | public func hash(into hasher: inout Hasher) {
96 | self.backgroundColor.hash(into: &hasher)
97 | self.id.hash(into: &hasher)
98 | self.width.hash(into: &hasher)
99 | self.height.hash(into: &hasher)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Sources/CollectionView/ComponentSupplementaryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 12.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import Foundation
8 | import LayoutKit
9 | import RxSwift
10 | import UIKit
11 |
12 | /**
13 | ComponentSupplementaryView is a UICollectionReusableView subclass that serves as the root UIView for the UI
14 | elements generated by its Layout.
15 | */
16 | public final class ComponentSupplementaryView: UICollectionReusableView {
17 |
18 | // MARK: Class Properties
19 | /**
20 | The String identifier used by the ComponentSupplementaryView to register to a UICollectionView instance.
21 | */
22 | public class var identifier: String {
23 | return String(describing: Mirror(reflecting: self).subjectType)
24 | }
25 |
26 | public override init(frame: CGRect) {
27 | super.init(frame: frame)
28 | self.serialDisposable.disposed(by: self.disposeBag)
29 | }
30 |
31 | public required init?(coder aDecoder: NSCoder) {
32 | fatalError("init(coder:) has not been implemented")
33 | }
34 |
35 | // MARK: Stored Properties
36 | /**
37 | The current ComponentLayout instance that created and arranged the subviews in this ComponentSupplementaryView.
38 | */
39 | private var layout: ComponentLayout?
40 |
41 | /**
42 | The UITapGestureRecognizer instance that handles user tap gestures, if there is one.
43 | */
44 | private var tap: UITapGestureRecognizer?
45 |
46 | /**
47 | The DisposeBag that manages Rx-related subscriptions.
48 | */
49 | private let disposeBag: DisposeBag = DisposeBag()
50 |
51 | /**
52 | The SerialDisposable that ensures there is only one subscription at a time for the UITapGestureRecognizer.
53 | */
54 | private let serialDisposable: SerialDisposable = SerialDisposable()
55 |
56 | // MARK: Overridden Properties
57 | public override final var intrinsicContentSize: CGSize {
58 | return self.sizeThatFits(
59 | CGSize(
60 | width: Constants.screenWidth,
61 | height: CGFloat.greatestFiniteMagnitude
62 | )
63 | )
64 | }
65 |
66 | // MARK: Overridden Methods
67 | public override func prepareForReuse() {
68 | super.prepareForReuse()
69 | self.subviews.forEach { $0.removeFromSuperview() }
70 | self.layout = nil
71 |
72 | if let tap = self.tap {
73 | self.removeGestureRecognizer(tap)
74 | }
75 |
76 | self.serialDisposable.disposable.dispose()
77 | self.tap = nil
78 | }
79 |
80 | public override final func sizeThatFits(_ size: CGSize) -> CGSize {
81 | guard let size = self.layout?.measurement(within: size).size else { return CGSize.zero }
82 | return size
83 | }
84 |
85 | /**
86 | Reads the layout from the AnyComponent instance to create the subviews in this ComponentSupplementaryView instance.
87 | This also sets its frame.size equal to its intrinsicContentSize and calls setNeedsLayout.
88 | - Parameters:
89 | - component: The AnyComponent instance that represents this ComponentSupplementaryView.
90 | */
91 | public func configure(with component: AnyComponent) {
92 | self.layout = component.layout
93 | self.frame.size = self.intrinsicContentSize
94 |
95 | if let selectable = component.identity.base as? Selectable {
96 | let tap: UITapGestureRecognizer = UITapGestureRecognizer()
97 |
98 | let disposable: Disposable = tap.rx.event.bind(onNext: { (_: UITapGestureRecognizer) -> Void in
99 | selectable.onSelect(self)
100 | })
101 |
102 | self.addGestureRecognizer(tap)
103 | self.serialDisposable.disposable = disposable
104 | self.tap = tap
105 | }
106 |
107 | self.layout?.arrangement(
108 | origin: self.bounds.origin,
109 | width: self.bounds.size.width,
110 | height: self.bounds.size.height
111 | )
112 | .makeViews(in: self)
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/Sources/Components/TextViewComponent/CyanicTextViewDelegateProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 22.05.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import UIKit
8 |
9 | internal final class CyanicTextViewDelegateProxy: NSObject {
10 |
11 | // Stored Properties
12 | /**
13 | The closure executed when the textViewShouldBeginEditing delegate method is called.
14 | */
15 | internal var shouldBeginEditing: (UITextView) -> Bool = { _ in return true }
16 |
17 | /**
18 | The closure executed when the textViewShouldEndEditing delegate method is called.
19 | */
20 | internal var shouldEndEditing: (UITextView) -> Bool = { _ in return true }
21 |
22 | /**
23 | The closure executed when the textViewDidBeginEditing delegate method is called.
24 | */
25 | internal var didBeginEditing: (UITextView) -> Void = { _ in }
26 |
27 | /**
28 | The closure executed when the textViewDidEndEditing delegate method is called.
29 | */
30 | internal var didEndEditing: (UITextView) -> Void = { _ in }
31 |
32 | /**
33 | The maximum number of characters allowed on the UITextView.
34 | */
35 | internal var maximumCharacterCount: Int = Int.max
36 |
37 | /**
38 | The closure executed when the textViewDidChange delegate method is called.
39 | */
40 | internal var didChange: (UITextView) -> Void = { _ in }
41 |
42 | /**
43 | The closure executed when the textViewDidChangeSelection delegate method is called.
44 | */
45 | internal var didChangeSelection: (UITextView) -> Void = { _ in }
46 |
47 | /**
48 | The closure executed when the textView:shouldInteractWithURL:characterRange:interaction delegate method is called.
49 | */
50 | internal var shouldInteractWithURLInCharacterRange: (UITextView, URL, NSRange, UITextItemInteraction) -> Bool = { _, _, _, _ in return true } // swiftlint:disable:this line_length
51 |
52 | /**
53 | The closure executed when the textView:shouldInteractWithTextAttachement:characterRange:interaction delegate
54 | method is called.
55 | */
56 | internal var shouldInteractWithTextAttachmentInCharacterRange: (UITextView, NSTextAttachment, NSRange, UITextItemInteraction) -> Bool = { _, _, _, _ in return true } // swiftlint:disable:this line_length
57 | }
58 |
59 | extension CyanicTextViewDelegateProxy: UITextViewDelegate {
60 | internal func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
61 | return self.shouldBeginEditing(textView)
62 | }
63 |
64 | internal func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
65 | return self.shouldEndEditing(textView)
66 | }
67 |
68 | internal func textViewDidBeginEditing(_ textView: UITextView) {
69 | self.didBeginEditing(textView)
70 | }
71 |
72 | internal func textViewDidEndEditing(_ textView: UITextView) {
73 | self.didEndEditing(textView)
74 | }
75 |
76 | internal func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
77 | let currentText: String = textView.text ?? ""
78 | guard let stringRange = Range(range, in: currentText) else { return false }
79 | let changedText: String = currentText.replacingCharacters(in: stringRange, with: text)
80 | return changedText.count <= self.maximumCharacterCount
81 | }
82 |
83 | internal func textViewDidChange(_ textView: UITextView) {
84 | self.didChange(textView)
85 | }
86 |
87 | internal func textViewDidChangeSelection(_ textView: UITextView) {
88 | self.didChangeSelection(textView)
89 | }
90 |
91 | internal func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
92 | return self.shouldInteractWithURLInCharacterRange(textView, URL, characterRange, interaction)
93 | }
94 |
95 | internal func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
96 | return self.shouldInteractWithTextAttachmentInCharacterRange(textView, textAttachment, characterRange, interaction)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Cyanic.xcodeproj/xcshareddata/xcschemes/Cyanic.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 |
78 |
79 |
80 |
81 |
87 |
88 |
94 |
95 |
96 |
97 |
99 |
100 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/Sources/Utilities/SectionController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 12.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import CoreGraphics
8 | import RxDataSources
9 |
10 | /**
11 | SectionController represents a section of a UICollectionView/UITableView. It consists of two main properties:
12 |
13 | * The Component that represents the supplementary view (in a UICollectionView) or a header view (in a UITableView) to
14 | be displayed in that section.
15 | * The ComponentsController that represents the items/rows to be displayed in that section.
16 |
17 | It is essentially the data source for a section in a UICollectionView/UITableView.
18 | */
19 | public struct SectionController: AnimatableSectionModelType {
20 |
21 | // MARK: Initializer
22 | /**
23 | Initializer.
24 | - Parameters:
25 | - width: The width of the UICollectionView/UITableView. SectionController mutates the width property of the
26 | headerComponent and initializes its ComponentsController instance with it.
27 | */
28 | public init(width: CGFloat) {
29 | self.width = width
30 | self.componentsController = ComponentsController(width: width)
31 | }
32 |
33 | public init(original: SectionController, items: [AnyComponent]) {
34 | self = original
35 | self.componentsController.components = items
36 | }
37 |
38 | // MARK: Stored Properties
39 | /**
40 | The width of the UICollectionView/UITableView where the components will be displayed.
41 | */
42 | public let width: CGFloat
43 |
44 | /**
45 | The Component representing the header view for the section in the UICollectionView/UITableView.
46 | */
47 | public var headerComponent: AnyComponent?
48 |
49 | /**
50 | The Component representing the footer view for the section in the UICollectionView/UITableView.
51 | */
52 | public var footerComponent: AnyComponent?
53 |
54 | /**
55 | The components for this section of the UICollectionView/UITableView.
56 | */
57 | public private(set) var componentsController: ComponentsController
58 |
59 | // MARK: Computed Properties
60 | public var identity: Int {
61 | var hasher: Hasher = Hasher()
62 | hasher.combine(self.headerComponent)
63 | hasher.combine(self.footerComponent)
64 | return hasher.finalize()
65 | }
66 |
67 | public var items: [AnyComponent] {
68 | return self.componentsController.components
69 | }
70 |
71 | /**
72 | The height of all the Components managed by this SectionController.
73 | */
74 | public var height: CGFloat {
75 | let componentsControllerHeight: CGFloat = self.componentsController.height
76 | let headerHeight: CGFloat = self.headerComponent?
77 | .layout
78 | .measurement(within: CGSize(width: self.width, height: CGFloat.greatestFiniteMagnitude))
79 | .size
80 | .height ?? 0.0
81 | let footerHeight: CGFloat = self.footerComponent?
82 | .layout
83 | .measurement(within: CGSize(width: self.width, height: CGFloat.greatestFiniteMagnitude))
84 | .size
85 | .height ?? 0.0
86 | return componentsControllerHeight + headerHeight + footerHeight
87 | }
88 |
89 | /**
90 | This method mutates the ComponentsController of this SectionController. Use this method to configure the
91 | ComponentController.
92 | - Parameters:
93 | - configuration: The closure contains the configuration logic to mutate the ComponentsController
94 | - componentsController: This SectionController's ComponentsController instance.
95 | */
96 | public mutating func buildComponents(_ configuration: (_ componentsController: inout ComponentsController) -> Void) {
97 | configuration(&self.componentsController)
98 | }
99 |
100 | }
101 |
102 | public extension SectionController {
103 |
104 | /**
105 | Represents the two options when adding a Component to a SectionContoller
106 | */
107 | enum SupplementaryView {
108 | /**
109 | Adds the Component as a header in the UICollectionView/UITableView
110 | */
111 | case header
112 |
113 | /**
114 | Adds the Component as a footer in the UICollectionView/UITableView
115 | */
116 | case footer
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/Sources/Classes/CyanicViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 21.03.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import Foundation
8 | import RxCocoa
9 | import RxSwift
10 | import UIKit
11 | import os
12 |
13 | internal let CyanicViewControllerLog: OSLog = OSLog(subsystem: Constants.bundleIdentifier, category: "CyanicViewController")
14 |
15 | /**
16 | CyanicViewController is a UIViewController subclass that can listen to State changes to its ViewModels. Whenever the
17 | State of at least one of its ViewModels changes, its invalidate method is called.
18 | */
19 | open class CyanicViewController: UIViewController, StateObservableBuilder {
20 |
21 | deinit {
22 | logDeallocation(of: self, log: CyanicViewControllerLog)
23 | }
24 |
25 | open override func viewDidLoad() {
26 | super.viewDidLoad()
27 | self.setUpObservables(with: self.viewModels)
28 | }
29 |
30 | // MARK: Stored Properties
31 | /**
32 | The serial scheduler where the ViewModel's state changes are observed on and mapped to the _components
33 | */
34 | internal let scheduler: SerialDispatchQueueScheduler = SerialDispatchQueueScheduler(
35 | qos: DispatchQoS.userInitiated,
36 | internalSerialQueueName: "\(UUID().uuidString)"
37 | )
38 |
39 | /**
40 | The combined state of the ViewModels as a BehviorRelay for debugging purposes.
41 | */
42 | internal let state: BehaviorRelay<[Any]> = BehaviorRelay<[Any]>(value: [()])
43 |
44 | /**
45 | DisposeBag for Rx-related subscriptions.
46 | */
47 | public let disposeBag: DisposeBag = DisposeBag()
48 |
49 | // MARK: Computed Properties
50 | /**
51 | Limits the frequency of state updates.
52 | */
53 | open var throttleType: ThrottleType { return ThrottleType.none }
54 |
55 | /**
56 | The current state of the ViewModels from the state BehaviorRelay.
57 | */
58 | public var currentState: Any { return self.state.value }
59 |
60 | /**
61 | The ViewModels whose State is observed by this CyanicViewController.
62 | */
63 | open var viewModels: [AnyViewModel] { return [] }
64 |
65 | internal typealias CombinedState = [Any]
66 |
67 | // MARK: Methods
68 | /**
69 | Creates an Observables based on ThrottleType and binds it to the invalidate method.
70 |
71 | It creates a new Observables based on the ViewModels' States and CyanicViewController ThrottleType and
72 | binds it to the invalidate method so any new State change calls the invalidate method.
73 |
74 | - Parameters:
75 | - viewModels: The ViewModels whose States will be observed.
76 | */
77 | @discardableResult
78 | internal func setUpObservables(with viewModels: [AnyViewModel]) -> Observable<[Any]> {
79 | guard !viewModels.isEmpty else { return Observable<[Any]>.empty() }
80 | let combinedStatesObservables: Observable<[Any]> = viewModels
81 | .combineStateObservables()
82 |
83 | var throttledStateObservable: Observable<[Any]> = self.setUpThrottleType(
84 | on: combinedStatesObservables,
85 | throttleType: self.throttleType,
86 | scheduler: self.scheduler
87 | )
88 | .observeOn(self.scheduler)
89 | .subscribeOn(self.scheduler)
90 |
91 | if viewModels.contains(where: { $0.isDebugMode }) {
92 | throttledStateObservable = throttledStateObservable
93 | .debug("\(type(of: self))", trimOutput: false)
94 | }
95 |
96 | throttledStateObservable = throttledStateObservable.share()
97 |
98 | throttledStateObservable
99 | .observeOn(MainScheduler.instance)
100 | .bind(
101 | onNext: { [weak self] (_: [Any]) -> Void in
102 | self?.invalidate()
103 | }
104 | )
105 | .disposed(by: self.disposeBag)
106 |
107 | throttledStateObservable
108 | .bind(to: self.state)
109 | .disposed(by: self.disposeBag)
110 |
111 | return throttledStateObservable
112 | }
113 |
114 | /**
115 | When the State of the ViewModel changes, invalidate is called, therefore, you should place logic here that
116 | should react to changes in state. This method is run on the main thread asynchronously.
117 |
118 | When overriding, no need to call super because the default implementation does nothing.
119 | */
120 | open func invalidate() {}
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/Example/ExampleList/ExampleListState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleState.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 3/27/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Cyanic
10 | import UIKit
11 |
12 | public struct ExampleListState: ExpandableState {
13 |
14 | // MARK: Enums
15 | public enum Expandable: String, CaseIterable {
16 | case first = "First Expandable"
17 | case second = "Second Expandable"
18 | }
19 |
20 | // MARK: Static Properties
21 | public static var `default`: ExampleListState {
22 | return ExampleListState(
23 | hasTextInTextField: true,
24 | expandableDict: ExampleListState.Expandable.allCases.map { $0.rawValue }
25 | .reduce(into: [String: Bool](), { (current, element) -> Void in
26 | current[element] = false
27 | }
28 | ),
29 | strings: [
30 | """
31 | Bacon ipsum dolor amet short ribs jerky spare ribs jowl, ham hock t-bone turkey capicola pork tenderloin. Rump t-bone ground round short loin ribeye alcatra pork chop spare ribs pancetta sausage chuck. Turducken pork sausage landjaeger t-bone. Kevin ground round tail ribeye pig drumstick alcatra bacon sausage.
32 | """,
33 | """
34 | Brisket shank pork loin filet mignon, strip steak landjaeger bacon. Fatback swine bresaola frankfurter sausage, bacon venison jowl salami pork loin beef ribs chuck. Filet mignon corned beef pig frankfurter short loin cow pastrami cupim ham. Turducken pancetta kevin salami, shank boudin pastrami meatball flank filet mignon kielbasa spare ribs.
35 |
36 | Doner ham pancetta sausage beef ribs flank tail filet mignon. Turducken leberkas jerky sirloin, tongue doner shank pastrami cupim. Alcatra pork loin prosciutto brisket meatloaf, beef ribs cow pork belly burgdoggen. Corned beef tail pork belly short loin chuck drumstick.
37 | """,
38 | """
39 | Doner ham pancetta sausage beef ribs flank tail filet mignon. Turducken leberkas jerky sirloin, tongue doner shank pastrami cupim. Alcatra pork loin prosciutto brisket meatloaf, beef ribs cow pork belly burgdoggen. Corned beef tail pork belly short loin chuck drumstick.
40 |
41 | Salami landjaeger pork chop, burgdoggen beef hamburger short loin alcatra filet mignon capicola tail. Swine cow pork ham hock turkey shoulder short loin porchetta tail buffalo meatloaf shank ham frankfurter. Shoulder pork belly tenderloin turkey ball tip drumstick porchetta. Meatball bresaola spare ribs porchetta shoulder andouille pork buffalo picanha swine beef ribs. T-bone meatloaf chicken capicola, strip steak doner turducken pancetta tenderloin short ribs jerky drumstick brisket.
42 |
43 | Shankle cow beef, rump buffalo short loin sirloin t-bone. Bresaola capicola pork pork loin drumstick turkey pig ball tip strip steak sausage landjaeger biltong short loin. Turkey rump shoulder tri-tip landjaeger, corned beef drumstick flank t-bone. Burgdoggen meatloaf pastrami spare ribs pork loin ham hock turkey.
44 | """
45 | ],
46 | otherStrings: [
47 | """
48 | Godfather ipsum dolor sit amet. You can act like a man! I don't trust a doctor who can hardly speak English. What's wrong with being a lawyer? Te salut, Don Corleone. That's my family Kay, that's not me.
49 | """,
50 | """
51 | Do me this favor. I won't forget it. Ask your friends in the neighborhood about me. They'll tell you I know how to return a favor. Why did you go to the police? Why didn't you come to me first? I'm your older brother, Mike, and I was stepped over! It's not personal. It's business. Just when I thought I was out... they pull me back in.
52 | """,
53 | """
54 | It's an old habit. I spent my whole life trying not to be careless. Women and children can afford to be careless, but not men. What's the matter with you? Is this what you've become, a Hollywood finocchio who cries like a woman? "Oh, what do I do? What do I do?" What is that nonsense? Ridiculous! You talk about vengeance. Is vengeance going to bring your son back to you? Or my boy to me? I don't like violence, Tom. I'm a businessman; blood is a big expense.
55 | """
56 | ],
57 | text: Async.uninitialized
58 |
59 | )
60 | }
61 |
62 | // MARK: Stored Properties
63 | public var hasTextInTextField: Bool
64 | public var expandableDict: [String: Bool]
65 | public var strings: [String]
66 | public var otherStrings: [String]
67 | public var text: Async
68 |
69 | }
70 |
71 |
--------------------------------------------------------------------------------
/Sources/Components/Protocols/TextFieldComponentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 09.04.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoEquatableComponent,AutoHashableComponent
11 | // sourcery: Component = TextFieldComponent,isFrameworkComponent
12 | /// TextFieldComponentType is a protocol for Components that represents a UITextField.
13 | public protocol TextFieldComponentType: StaticHeightComponent {
14 |
15 | // sourcery: defaultValue = """"
16 | /// The String displayed as placeholder text on the UITextField. The default value is an empty string: "".
17 | var placeholder: String { get set }
18 |
19 | // sourcery: defaultValue = """"
20 | // sourcery: skipHashing, skipEquality
21 | /// The String displayed as text on the UITextField. The default value is an empty string: "".
22 | var text: String { get set }
23 |
24 | // sourcery: defaultValue = UIEdgeInsets.zero
25 | // sourcery: skipHashing, skipEquality
26 | /// The insets on the UITextField relative to its root UIView.
27 | /// UIButton. The default value is UIEdgeInsets.zero.
28 | var insets: UIEdgeInsets { get set }
29 |
30 | // sourcery: defaultValue = UIColor.clear
31 | /// The background color of the UITextField. The default value is UIColor.clear.
32 | var backgroundColor: UIColor { get set }
33 |
34 | // sourcery: defaultValue = Alignment.centerLeading
35 | // sourcery: skipHashing, skipEquality
36 | /// The alignment of the underlying SizeLayout. The default value is Alignment.centerLeading.
37 | var alignment: Alignment { get set }
38 |
39 | // sourcery: defaultValue = Flexibility.flexible
40 | // sourcery: skipHashing, skipEquality
41 | /// The flexibility of the underlying SizeLayout. The default value is Flexibility.flexible.
42 | var flexibility: Flexibility { get set }
43 |
44 | // sourcery: defaultValue = "500"
45 | /// The number of milliseconds that must elapse before editingChanged is called. The default value is 500 milliseconds.
46 | var editingChangeDelay: Int { get set }
47 |
48 | // sourcery: defaultValue = "{ _ in }"
49 | // sourcery: skipHashing, skipEquality
50 | /// The configuration applied to the UITextField. The default closure does nothing.
51 | var configuration: (UITextField) -> Void { get set }
52 |
53 | // sourcery: defaultValue = "{ (_: UITextField) -> Void in print("TextField has new text") }"
54 | // sourcery: skipHashing, skipEquality
55 | /// This closure is executed 0.5 seconds after the user has stopped typing on the UITextField.
56 | var editingChanged: (UITextField) -> Void { get set }
57 |
58 | // sourcery: defaultValue = "UITextField.self"
59 | // sourcery: skipHashing, skipEquality
60 | var textFieldType: UITextField.Type { get set }
61 |
62 | // sourcery: defaultValue = "CyanicTextFieldDelegateProxy()"
63 | // sourcery: skipHashing, skipEquality
64 | /// The UITextFieldDelegate for the underlying UITextField. This cannot be set, Cyanic takes care of the
65 | /// implementation. Use the closures to customize functionality.
66 | var delegate: UITextFieldDelegate { get }
67 |
68 | // sourcery: defaultValue = "{ _ in return true }"
69 | // sourcery: skipHashing, skipEquality
70 | /// The closure executed when the textFieldShouldBeginEditing delegate method is called.
71 | var shouldBeginEditing: (UITextField) -> Bool { get set }
72 |
73 | // sourcery: defaultValue = "{ _ in }"
74 | // sourcery: skipHashing, skipEquality
75 | /// The closure executed when the textFieldDidBeginEditing delegate method is called.
76 | var didBeginEditing: (UITextField) -> Void { get set }
77 |
78 | // sourcery: defaultValue = "{ _ in return true }"
79 | // sourcery: skipHashing, skipEquality
80 | /// The closure executed when the textFieldShouldEndEditing delegate method is called.
81 | var shouldEndEditing: (UITextField) -> Bool { get set }
82 |
83 | // sourcery: defaultValue = "{ _ in }"
84 | // sourcery: skipHashing, skipEquality
85 | /// The closure executed when the textFieldDidEndEditing delegate method is called.
86 | var didEndEditing: (UITextField) -> Void { get set }
87 |
88 | // sourcery: defaultValue = "Int.max"
89 | /// The maximum number of characters allowed on the UITextField.
90 | var maximumCharacterCount: Int { get set }
91 |
92 | // sourcery: defaultValue = "{ _ in return true }"
93 | // sourcery: skipHashing, skipEquality
94 | /// The closure executed when the textFieldShouldClear delegate method is called.
95 | var shouldClear: (UITextField) -> Bool { get set }
96 |
97 | // sourcery: defaultValue = "{ _ in return true }"
98 | // sourcery: skipHashing, skipEquality
99 | /// The closure executed when the textFieldShouldReturn delegate method is called.
100 | var shouldReturn: (UITextField) -> Bool { get set }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cyanic
2 |
3 | Cyanic is an iOS framework created at [Feil, Feil, & Feil GmbH](https://ffuf.de/en/) in response to a need for state-driven UI. It borrows heavily
4 | from the concepts of Airbnb's [MvRx](https://github.com/airbnb/MvRx) framework (which our Android developers use) to create a very similar
5 | code base with Android thereby unifying the business logic in both platforms. We use this framework to create complex, performant, and reactive
6 | screens in our projects.
7 |
8 | Cyanic is a Swift only framework. There are no plans to make it compatible with Objective-C.
9 |
10 | ## Installation
11 | ### [CocoaPods](http://cocoapods.org/)
12 |
13 | Requirements:
14 | * Swift 5.0+
15 | * iOS 10.0+
16 |
17 | 1. Add the following to your [Podfile](http://guides.cocoapods.org/using/the-podfile.html):
18 | ```rb
19 | pod 'Cyanic'
20 | pod 'LayoutKit', :git => 'https://github.com/hooliooo/LayoutKit.git' // Use this fork until LayoutKit is updated
21 | ```
22 |
23 | 2. Integrate your dependencies using frameworks: add `use_frameworks!` to your Podfile.
24 | 3. Run `pod install`.
25 |
26 | ## Why we use a forked version of LayoutKit
27 |
28 | LayoutKit is the library that is responsible for most of the UI logic in Cyanic. However, as of April 17, 2019, there are some limitations to the current LayoutKit version in Cocoapods:
29 |
30 | 1. It is not updated to use Swift 5
31 | 2. Cyanic needs access to an internal initializer of the Layouts that allows you to declare the UIView subclass type as an argument.
32 |
33 | Without these changes, Cyanic will continue to use the forked version.
34 |
35 | ## Documentation
36 |
37 | Check out our [wiki](https://github.com/feilfeilundfeil/Cyanic/wiki) for full documentation.
38 |
39 | ## A Simple Example
40 |
41 | A very simple example with expandable functionality:
42 |
43 | ```swift
44 | struct YourState: ExpandableState {
45 |
46 | enum Section: String, CaseIterable {
47 | case first
48 | case second
49 | }
50 |
51 | static var `default`: YourState {
52 | return YourState(
53 | text: "Hello, World!",
54 | expandableDict: YourState.Section.allCases.map { $0.rawValue }
55 | .reduce(into: [String: Bool](), { (current: inout [String: Bool], id: String) -> Void in
56 | current[id] = false
57 | }
58 | )
59 | }
60 |
61 | var text: String
62 | var expandableDict: [String: Bool]
63 | }
64 |
65 | class YourViewModel: ViewModel {
66 | func showCyanic() {
67 | self.setState { $0.text = "Hello, Cyanic!" }
68 | }
69 | }
70 |
71 | class YourComponentViewController: SingleSectionCollectionComponentViewController {
72 |
73 | private let viewModel: YourViewModel = YourViewModel(initialState: YourState.default)
74 |
75 | override var viewModels: [AnyViewModel] {
76 | return [self.viewModel.asAnyViewModel]
77 | }
78 |
79 | override func buildComponents(_ componentsController: inout ComponentsController) {
80 | withState(self.viewModel) { (state: YourState) -> Void in
81 | componentsController.staticTextComponent {
82 | $0.id = "title"
83 | $0.text = state.text
84 | }
85 |
86 | componentsController.buttonComponent {
87 | $0.id = "button"
88 | $0.onTap = { [weak self]
89 | self?.viewModel.showCyanic()
90 | }
91 | }
92 |
93 | let firstExpandableID: String = YourState.Section.first.rawValue
94 |
95 | let yourExpandable = components.expandableComponent { [weak self] in
96 | guard let s = self else { return }
97 | $0.id = firstExpandableID
98 | $0.contentLayout = LabelContentLayout(text: Text.unattributed("Hello, World!"))
99 | $0.isExpanded = state.expandableDict[firstExpandableID] ?? false
100 | $0.setExpandableState = self.viewModel.setExpandableState
101 | $0.backgroundColor = UIColor.lightGray
102 | $0.height = 55.0
103 | }
104 |
105 | // These ButtonComponents will only show up when yourExpandable is expanded.
106 | if yourExpandable.isExpanded {
107 | for number in 1...5 {
108 | componentsController.buttonComponent {
109 | $0.id = "button\(number)"
110 | $0.title = "\(number)"
111 | $0.onTap = { [weak self]
112 | print("Hello, World from Button \(number)")
113 | }
114 | }
115 | }
116 | }
117 | }
118 | }
119 | }
120 | ```
121 |
122 | ## Contributors
123 |
124 | * Julio Alorro (julio.alorro@ffuf.de)
125 | * Jonas Bark (jonas.bark@ffuf.de)
126 | * Alexander Korus (alexander.korus@ffuf.de)
127 |
--------------------------------------------------------------------------------
/Example/ExampleList/CompositeVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompositeVC.swift
3 | // Example
4 | //
5 | // Created by Julio Miguel Alorro on 3/10/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Cyanic
10 | import Alacrity
11 | import LayoutKit
12 | import RxCocoa
13 | import RxSwift
14 |
15 | class ExampleLoginVC: ComponentViewController {
16 |
17 | init(viewModelOne: ViewModelA, viewModelTwo: ViewModelB) {
18 | self.viewModelOne = viewModelOne
19 | self.viewModelTwo = viewModelTwo
20 | super.init(nibName: nil, bundle: nil)
21 | }
22 |
23 | required init?(coder aDecoder: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | deinit {
28 | print("CompositeVC Deallocated")
29 | }
30 |
31 | override func viewDidLoad() {
32 | super.viewDidLoad()
33 | // self.navigationController?.isNavigationBarHidden = true
34 | self.view.backgroundColor = UIColor.white
35 |
36 | self.kio.setUpNavigationItem {
37 | $0.rightBarButtonItems = [
38 | UIBarButtonItem(
39 | title: "Second", style: UIBarButtonItem.Style.plain,
40 | target: self,
41 | action: #selector(ExampleLoginVC.addButtonTapped)
42 | ),
43 | UIBarButtonItem(
44 | title: "Third", style: UIBarButtonItem.Style.plain,
45 | target: self,
46 | action: #selector(ExampleLoginVC.otherButtonTapped)
47 | )
48 | ].reversed()
49 | }
50 | }
51 |
52 | let viewModelOne: ViewModelA
53 | let viewModelTwo: ViewModelB
54 |
55 | override var throttleType: ThrottleType { return ThrottleType.debounce(0.1) }
56 | override var width: ComponentViewController.Width { return .exactly(240.0) }
57 | override var viewModels: [AnyViewModel] {
58 | return [AnyViewModel(self.viewModelOne), AnyViewModel(self.viewModelTwo)]
59 | }
60 |
61 | @objc func addButtonTapped() {
62 | self.viewModelOne.addButtonTapped()
63 | self.viewModelTwo.addButtonTapped()
64 | }
65 |
66 | @objc func otherButtonTapped() {
67 | self.viewModelOne.otherButtonTapped()
68 | self.viewModelTwo.otherButtonTapped()
69 | }
70 |
71 | override func buildComponents(_ componentsController: inout ComponentsController) {
72 | let width: CGFloat = componentsController.width
73 |
74 | withState(
75 | viewModel1: self.viewModelOne,
76 | viewModel2: self.viewModelTwo
77 | ) { (state1: StateA, state2: StateB) -> Void in
78 | let isTrue: Bool = state1.isTrue && state2.isTrue
79 | componentsController.staticTextComponent {
80 | $0.id = "First"
81 | $0.text = Text.unattributed(isTrue ? "This should say true when both are true" : "False")
82 | $0.backgroundColor = UIColor.red
83 | $0.style = AlacrityStyle { $0.textColor = UIColor.black }
84 | $0.font = UIFont.systemFont(ofSize: 17.0)
85 | $0.insets = UIEdgeInsets(top: 20.0, left: 20.0, bottom: 20.0, right: 20.0)
86 | $0.width = width
87 | }
88 |
89 | componentsController.add(
90 | StaticTextComponent(id: "Second")
91 | .copy {
92 | $0.text = Text.unattributed(state1.isTrue ? "First state is true" : "False")
93 | $0.backgroundColor = UIColor.gray
94 | $0.style = AlacrityStyle { $0.textColor = UIColor.black }
95 | $0.font = UIFont.systemFont(ofSize: 17.0)
96 | $0.insets = UIEdgeInsets(top: 20.0, left: 20.0, bottom: 20.0, right: 20.0)
97 | $0.width = width
98 | }
99 | )
100 |
101 | componentsController.add(
102 | StaticTextComponent(id: "Third")
103 | .copy {
104 | $0.text = Text.unattributed(state2.isTrue ? "Second state is true" : "False")
105 | $0.backgroundColor = UIColor.green
106 | $0.style = AlacrityStyle { $0.textColor = UIColor.black }
107 | $0.font = UIFont.systemFont(ofSize: 17.0)
108 | $0.insets = UIEdgeInsets(top: 20.0, left: 20.0, bottom: 20.0, right: 20.0)
109 | $0.width = width
110 | }
111 | )
112 |
113 | componentsController.staticSpacingComponent {
114 | $0.id = "Blah"
115 | $0.backgroundColor = UIColor.yellow
116 | $0.height = 44.0
117 | }
118 | }
119 | }
120 |
121 | override func invalidate() {
122 | withState(viewModel1: self.viewModelOne, viewModel2: self.viewModelTwo) { (state1: StateA, state2: StateB) -> Void in
123 | switch state1.isTrue {
124 | case true:
125 | self.topAnchorConstraint.constant = -20.0
126 | case false:
127 | self.topAnchorConstraint.constant = 0.0
128 | }
129 |
130 | self.view.layoutIfNeeded()
131 | }
132 | }
133 |
134 | }
135 |
--------------------------------------------------------------------------------
/Sources/Components/ExpandableComponent/ExpandableComponentLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 15.02.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | /**
11 | The ExpandableComponentLayout is a ComponentLayout that is a subclass of SizeLayout.
12 | Used to create, size, and arrange the subviews associated with ExpandableComponent.
13 | */
14 | public final class ExpandableComponentLayout: OverlayLayout, ComponentLayout {
15 |
16 | /**
17 | Initializer
18 | - Parameters:
19 | - component: ExpandableComponent instance. Properties from this instance are used to configure the view's
20 | appearance and determine the size of the content.
21 | */
22 | public init(component: ExpandableComponent) { // swiftlint:disable:this function_body_length
23 | let size: CGSize = component.size
24 | let insets: UIEdgeInsets = component.insets
25 | let contentInsetLayout: InsetLayout = InsetLayout(
26 | insets: UIEdgeInsets(top: insets.top, left: insets.left, bottom: insets.bottom, right: 0.0),
27 | viewReuseId: "\(ExpandableComponentLayout.identifier)RealContentInset",
28 | sublayout: component.contentLayout
29 | )
30 |
31 | let accessorySizeLayout: SizeLayout = SizeLayout(
32 | minWidth: component.accessoryViewSize.width,
33 | maxWidth: component.accessoryViewSize.width,
34 | minHeight: component.accessoryViewSize.height,
35 | maxHeight: component.accessoryViewSize.height,
36 | alignment: Alignment.center,
37 | flexibility: Flexibility.inflexible,
38 | viewReuseId: "\(ExpandableComponentLayout.identifier)AccessorySize",
39 | viewClass: component.accessoryViewType,
40 | config: component.accessoryViewConfiguration
41 | )
42 |
43 | let accessoryInsetLayout: InsetLayout = InsetLayout(
44 | insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: insets.right),
45 | alignment: Alignment.center,
46 | flexibility: accessorySizeLayout.flexibility,
47 | viewReuseId: "\(ExpandableComponentLayout.identifier)AccessoryInset",
48 | sublayout: accessorySizeLayout
49 | )
50 |
51 | let adjustedSize: CGSize = CGSize(
52 | width: size.width - insets.left - insets.right,
53 | height: component.height - insets.top - insets.bottom
54 | )
55 |
56 | let contentWidth: CGFloat = contentInsetLayout.measurement(within: adjustedSize).size.width
57 | let accessoryWidth: CGFloat = accessoryInsetLayout.measurement(within: adjustedSize).size.width
58 |
59 | let spacing: CGFloat = size.width - contentWidth - accessoryWidth
60 |
61 | let horizontalStack: StackLayout = StackLayout(
62 | axis: Axis.horizontal,
63 | spacing: spacing,
64 | distribution: StackLayoutDistribution.fillEqualSpacing,
65 | alignment: Alignment.fillLeading,
66 | flexibility: Flexibility.flexible,
67 | viewReuseId: "\(ExpandableComponentLayout.identifier)HorizontalStack",
68 | sublayouts: [contentInsetLayout, accessoryInsetLayout]
69 | )
70 |
71 | let sizeLayout: SizeLayout = SizeLayout(
72 | minWidth: component.width,
73 | maxWidth: component.width,
74 | minHeight: component.height,
75 | maxHeight: component.height,
76 | flexibility: Flexibility.inflexible,
77 | viewReuseId: "\(ExpandableComponentLayout.identifier)SizeLayout",
78 | sublayout: horizontalStack
79 | )
80 |
81 | var overlayLayouts: [Layout] = []
82 |
83 | if let dividerLine = component.dividerLine {
84 |
85 | let sizeLayout: SizeLayout = SizeLayout(
86 | minWidth: component.width - dividerLine.insets.left - dividerLine.insets.right,
87 | maxWidth: component.width,
88 | minHeight: dividerLine.height,
89 | maxHeight: dividerLine.height,
90 | alignment: Alignment.bottomCenter,
91 | flexibility: Flexibility.inflexible,
92 | viewReuseId: "dividerLine",
93 | sublayout: nil,
94 | config: { (view: UIView) -> Void in
95 | view.backgroundColor = dividerLine.backgroundColor
96 | }
97 | )
98 |
99 | let dividerLineInsetLayout: InsetLayout = InsetLayout(
100 | insets: dividerLine.insets,
101 | sublayout: sizeLayout
102 | )
103 |
104 | overlayLayouts.append(dividerLineInsetLayout)
105 | }
106 |
107 | super.init(
108 | primaryLayouts: [sizeLayout],
109 | backgroundLayouts: [],
110 | overlayLayouts: overlayLayouts,
111 | alignment: Alignment.centerLeading,
112 | flexibility: Flexibility.flexible,
113 | viewReuseId: ExpandableComponentLayout.identifier,
114 | config: { (view: UIView) -> Void in
115 | component.configuration(view)
116 | view.backgroundColor = component.backgroundColor
117 | }
118 | )
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Sources/Components/Protocols/TextViewComponentType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cyanic
3 | // Created by Julio Miguel Alorro on 16.05.19.
4 | // Licensed under the MIT license. See LICENSE file
5 | //
6 |
7 | import LayoutKit
8 | import UIKit
9 |
10 | // sourcery: AutoEquatableComponent,AutoHashableComponent
11 | // sourcery: Component = TextViewComponent,isFrameworkComponent
12 | /// TextViewComponentType is a protocol for Components that represents a UITextView.
13 | public protocol TextViewComponentType: StaticHeightComponent {
14 |
15 | // sourcery: defaultValue = """"
16 | // sourcery: skipHashing, skipEquality
17 | /// The String displayed as text on the UITextView. The default value is an empty string: "".
18 | var text: String { get set }
19 |
20 | // sourcery: defaultValue = "UIFont.systemFont(ofSize: 13.0)"
21 | // sourcery: skipHashing, skipEquality
22 | /// The font of the Text. The default value is UIFont.systemFont(ofSize: 13.0).
23 | var font: UIFont { get set }
24 |
25 | // sourcery: defaultValue = UIEdgeInsets.zero
26 | // sourcery: skipHashing, skipEquality
27 | /// The insets on the UITextView relative to its root UIView. The default value is UIEdgeInsets.zero.
28 | var insets: UIEdgeInsets { get set }
29 |
30 | // sourcery: defaultValue = UIEdgeInsets.zero
31 | // sourcery: skipHashing, skipEquality
32 | /// The textContainerInset on the UITextView. The default value is UIEdgeInsets.zero.
33 | var textContainerInset: UIEdgeInsets { get set }
34 |
35 | // sourcery: defaultValue = UIColor.clear
36 | /// The background color of the UITextView. The default value is UIColor.clear.
37 | var backgroundColor: UIColor { get set }
38 |
39 | // sourcery: defaultValue = Alignment.fill
40 | // sourcery: skipHashing, skipEquality
41 | /// The alignment of the underlying SizeLayout. The default value is Alignment.fill.
42 | var alignment: Alignment { get set }
43 |
44 | // sourcery: defaultValue = Flexibility.flexible
45 | // sourcery: skipHashing, skipEquality
46 | /// The flexibility of the underlying SizeLayout. The default value is Flexibility.flexible.
47 | var flexibility: Flexibility { get set }
48 |
49 | // sourcery: defaultValue = "{ _ in }"
50 | // sourcery: skipHashing, skipEquality
51 | /// The configuration applied to the UITextField. The default closure does nothing.
52 | var configuration: (UITextView) -> Void { get set }
53 |
54 | // sourcery: defaultValue = "UITextView.self"
55 | // sourcery: skipHashing, skipEquality
56 | var textViewType: UITextView.Type { get set }
57 |
58 | // sourcery: defaultValue = "CyanicTextViewDelegateProxy()"
59 | // sourcery: skipHashing, skipEquality
60 | /// The UITextViewDelegate for the underlying UITextView. This cannot be set, Cyanic takes care of the
61 | /// implementation. Use the closures to customize functionality.
62 | var delegate: UITextViewDelegate { get }
63 |
64 | // sourcery: defaultValue = "{ _ in return true }"
65 | // sourcery: skipHashing, skipEquality
66 | /// The closure executed when the textViewShouldBeginEditing delegate method is called.
67 | var shouldBeginEditing: (UITextView) -> Bool { get set }
68 |
69 | // sourcery: defaultValue = "{ _ in return true }"
70 | // sourcery: skipHashing, skipEquality
71 | /// The closure executed when the textViewShouldEndEditing delegate method is called.
72 | var shouldEndEditing: (UITextView) -> Bool { get set }
73 |
74 | // sourcery: defaultValue = "{ _ in }"
75 | // sourcery: skipHashing, skipEquality
76 | /// The closure executed when the textViewDidBeginEditing delegate method is called.
77 | var didBeginEditing: (UITextView) -> Void { get set }
78 |
79 | // sourcery: defaultValue = "{ _ in }"
80 | // sourcery: skipHashing, skipEquality
81 | /// The closure executed when the textViewDidEndEditing delegate method is called.
82 | var didEndEditing: (UITextView) -> Void { get set }
83 |
84 | // sourcery: defaultValue = "Int.max"
85 | // sourcery: skipHashing, skipEquality
86 | /// The maximum number of characters allowed on the UITextView.
87 | var maximumCharacterCount: Int { get set }
88 |
89 | // sourcery: defaultValue = "{ _ in }"
90 | // sourcery: skipHashing, skipEquality
91 | /// The closure executed when the textViewDidChange delegate method is called.
92 | var didChange: (UITextView) -> Void { get set }
93 |
94 | // sourcery: defaultValue = "{ _ in }"
95 | // sourcery: skipHashing, skipEquality
96 | /// The closure executed when the textViewDidChangeSelection delegate method is called.
97 | var didChangeSelection: (UITextView) -> Void { get set }
98 |
99 | // sourcery: defaultValue = " { _, _, _, _ in return true }"
100 | // sourcery: skipHashing, skipEquality
101 | /// The closure executed when the textView:shouldInteractWithURL:characterRange:interaction delegate method is called.
102 | var shouldInteractWithURLInCharacterRange: (UITextView, URL, NSRange, UITextItemInteraction) -> Bool { get set } // swiftlint:disable:this line_length
103 |
104 | // sourcery: defaultValue = " { _, _, _, _ in return true }"
105 | // sourcery: skipHashing, skipEquality
106 | /// The closure executed when the textView:shouldInteractWithTextAttachement:characterRange:interaction delegate
107 | /// method is called.
108 | var shouldInteractWithTextAttachmentInCharacterRange: (UITextView, NSTextAttachment, NSRange, UITextItemInteraction) -> Bool { get set }
109 | // swiftlint:disable:previous line_length
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/Tests/StateStoreTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StateStoreTests.swift
3 | // Tests
4 | //
5 | // Created by Julio Miguel Alorro on 3/21/19.
6 | // Copyright © 2019 Feil, Feil, & Feil GmbH. All rights reserved.
7 | //
8 |
9 | import Quick
10 | import Nimble
11 | @testable import Cyanic
12 |
13 | class StateStoreTests: QuickSpec {
14 |
15 | struct TestState: State {
16 |
17 | static var `default`: StateStoreTests.TestState {
18 | return TestState(count: 0, string: "Hello, World")
19 | }
20 |
21 | var count: Int
22 | var string: String
23 | }
24 |
25 | func createStore() -> StateStore {
26 | return StateStore(initialState: TestState.default)
27 | }
28 |
29 | override func spec() {
30 | describe("getState method") {
31 | let store: StateStore = self.createStore()
32 | it("should be asynchronous") {
33 | var count: Int = 0
34 | store.getState(with: { _ in count += 1})
35 | expect(count).to(equal(0))
36 | expect(count).toEventually(equal(1))
37 | }
38 | }
39 |
40 | describe("setState method") {
41 | let store: StateStore = self.createStore()
42 | it("should be asynchronous and setState should be resolved before getState") {
43 | var count: Int = 0
44 | store.getState(with: { count = $0.count })
45 | store.setState(with: { $0.count = 2})
46 | expect(count).to(equal(0))
47 | expect(count).toEventually(equal(2))
48 | }
49 | }
50 |
51 | describe("state observable") {
52 | let store: StateStore = self.createStore()
53 | it("should only change if the new State is different from the old State") {
54 | var count: Int = 0
55 | _ = store.state
56 | .bind(onNext: { _ in count += 1})
57 |
58 | expect(count).to(equal(1)) // Incremented due to the fact that BehaviorRelay
59 | // replays it's current value on subscribe
60 | store.setState(with: { $0 = $0})
61 | expect(count).to(equal(1))
62 | store.setState(with: { $0.count = 1 })
63 | expect(count).toEventually(equal(2))
64 | }
65 | }
66 |
67 | describe("stress test") {
68 | let store: StateStore = self.createStore()
69 | it("should be able to handle concurrency") {
70 | var concurrentQueue1Count: [Int] = []
71 | var concurrentQueue2Count: [Int] = []
72 |
73 | let concurrentQueue: DispatchQueue = DispatchQueue(
74 | label: "Concurrent1",
75 | qos: DispatchQoS.default,
76 | attributes: .concurrent
77 | )
78 |
79 | let firstLoopStart: Int = 0
80 | let firstLoopEnd: Int = 1_000
81 |
82 | let secondLoopStart: Int = firstLoopEnd + 1
83 | let secondLoopEnd: Int = firstLoopEnd * 2
84 |
85 | var finalCount: Int = 0
86 |
87 | concurrentQueue.async {
88 | for num in secondLoopStart...secondLoopEnd {
89 | store.setState(with: {
90 | $0.count = num
91 | print("Async Num from Second Loop: \(num)")
92 | concurrentQueue2Count.append(num)
93 |
94 | store.setState(with: {
95 | $0.string = "\(Int.random(in: Int.min...Int.max))"
96 | store.setState(with: {
97 | $0.count = num
98 | print("Async Num from Second Loop: \(num)")
99 | concurrentQueue2Count.append(num)
100 |
101 | store.setState(with: {
102 | $0.string = "\(Int.random(in: Int.min...Int.max))"
103 |
104 | })
105 | })
106 | })
107 | })
108 | }
109 | }
110 |
111 | concurrentQueue.async {
112 | for _ in firstLoopStart...firstLoopEnd {
113 | store.getState(with: {
114 | finalCount = $0.count
115 | })
116 | }
117 | }
118 |
119 | concurrentQueue.async {
120 | for num in firstLoopStart...firstLoopEnd {
121 | store.setState(with: {
122 | $0.count = num
123 | print("Async Num from First Loop: \(num)")
124 | concurrentQueue1Count.append(num)
125 | })
126 | }
127 | }
128 |
129 | expect(concurrentQueue1Count).toEventually(contain(firstLoopEnd), description: "Stress count for async loop")
130 | expect(concurrentQueue2Count).toEventually(contain(secondLoopEnd), description: "Stress count for async loop")
131 | expect(finalCount).toEventually(satisfyAnyOf(equal(firstLoopEnd), equal(secondLoopEnd)), description: "\(finalCount) should equal \(firstLoopEnd) or \(secondLoopEnd)")
132 |
133 | }
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------