From 671000c2177ab0d3d5ce5ae7f9c96c3bd0fe6e43 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Mon, 2 Apr 2018 20:39:07 +0300 Subject: [PATCH] Add PinLayout` dependency. Add PinLayoutTableViewCell, SeparatorTableViewCell and LabelTableViewCell powered by PinLayout. Add LabelCellViewModel default view model for label cell. Add Playground to project. --- CHANGELOG.md | 6 + LeadKitAdditions.podspec | 2 +- LeadKitAdditions.xcodeproj/project.pbxproj | 62 ++++++++ Podfile | 1 + Podfile.lock | 15 +- .../LabelCellViewModel.swift | 56 +++++++ .../LabelTableViewCell.swift | 141 ++++++++++++++++++ .../Cells/PinLayoutTableViewCell.swift | 104 +++++++++++++ .../Cells/SeparatorTableViewCell.swift | 104 +++++++++++++ .../Cells/LabelTableViewCell+Extensions.swift | 31 ++++ .../PinLayoutCell+DefaultImplementation.swift | 33 ++++ Sources/Protocols/Cells/PinLayoutCell.swift | 60 ++++++++ iOS.playground/Contents.swift | 4 + ...bal_all_rounded_background_radius_8@3x.png | Bin 0 -> 19479 bytes iOS.playground/contents.xcplayground | 4 + 15 files changed, 618 insertions(+), 5 deletions(-) create mode 100644 Sources/Classes/Cells/LabelTableViewCell/LabelCellViewModel.swift create mode 100644 Sources/Classes/Cells/LabelTableViewCell/LabelTableViewCell.swift create mode 100644 Sources/Classes/Cells/PinLayoutTableViewCell.swift create mode 100644 Sources/Classes/Cells/SeparatorTableViewCell.swift create mode 100644 Sources/Extensions/Cells/LabelTableViewCell+Extensions.swift create mode 100644 Sources/Extensions/Cells/PinLayoutCell+DefaultImplementation.swift create mode 100644 Sources/Protocols/Cells/PinLayoutCell.swift create mode 100644 iOS.playground/Contents.swift create mode 100644 iOS.playground/Resources/common_global_all_rounded_background_radius_8@3x.png create mode 100644 iOS.playground/contents.xcplayground diff --git a/CHANGELOG.md b/CHANGELOG.md index c1bb4fb..e3c0422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 0.2.4 +- **Add**: `PinLayout` dependency. +- **Add**: `PinLayoutTableViewCell`, `SeparatorTableViewCell` and `LabelTableViewCell` powered by PinLayout. +- **Add**: `LabelCellViewModel` default view model for label cell. +- **Add**: Playground to project. + ### 0.2.3 - **Update**: Xcode 9.3 migration. diff --git a/LeadKitAdditions.podspec b/LeadKitAdditions.podspec index c84c52c..15f68b5 100644 --- a/LeadKitAdditions.podspec +++ b/LeadKitAdditions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "LeadKitAdditions" - s.version = "0.2.3" + s.version = "0.2.4" s.summary = "iOS framework with a bunch of tools for rapid development" s.homepage = "https://github.com/TouchInstinct/LeadKitAdditions" s.license = "Apache License, Version 2.0" diff --git a/LeadKitAdditions.xcodeproj/project.pbxproj b/LeadKitAdditions.xcodeproj/project.pbxproj index 5c30bb7..cfc6b2e 100644 --- a/LeadKitAdditions.xcodeproj/project.pbxproj +++ b/LeadKitAdditions.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 248389A288C0A6D7914F0546 /* Pods_LeadKitAdditions_LeadKitAdditions_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0ED4A1B793EAA73C9E95969F /* Pods_LeadKitAdditions_LeadKitAdditions_iOS.framework */; }; + 6760AF1A207268EC00C2BB7E /* PinLayoutCell+DefaultImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6760AF19207268EC00C2BB7E /* PinLayoutCell+DefaultImplementation.swift */; }; 67779CBC206986390098F024 /* BaseTextFieldViewEvents+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67779CBB206986390098F024 /* BaseTextFieldViewEvents+Extensions.swift */; }; 67779CBD206986390098F024 /* BaseTextFieldViewEvents+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67779CBB206986390098F024 /* BaseTextFieldViewEvents+Extensions.swift */; }; 678D26AA206935B900B05B93 /* BiometricsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 678D26A9206935B900B05B93 /* BiometricsService.swift */; }; @@ -18,6 +19,12 @@ 67B4E6F9206945F900E233EA /* OnlineValidationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B4E6F8206945F900E233EA /* OnlineValidationState.swift */; }; 67B4E6FA206945F900E233EA /* OnlineValidationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B4E6F8206945F900E233EA /* OnlineValidationState.swift */; }; 67B4E6FB20694A4200E233EA /* BaseTextFieldViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B4E6F1206945D200E233EA /* BaseTextFieldViewModel.swift */; }; + 67C2A41620724BBA000A5682 /* SeparatorTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67C2A41520724BBA000A5682 /* SeparatorTableViewCell.swift */; }; + 67C2A41820724EA0000A5682 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67C2A41720724EA0000A5682 /* LabelTableViewCell.swift */; }; + 67C2A41B20724F40000A5682 /* LabelCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67C2A41A20724F40000A5682 /* LabelCellViewModel.swift */; }; + 67C2A41D20725359000A5682 /* LabelTableViewCell+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67C2A41C20725359000A5682 /* LabelTableViewCell+Extensions.swift */; }; + 67CF05AA206E9880009A2AB9 /* PinLayoutCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67CF05A9206E9880009A2AB9 /* PinLayoutCell.swift */; }; + 67CF05B0206E99DF009A2AB9 /* PinLayoutTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67CF05AF206E99DF009A2AB9 /* PinLayoutTableViewCell.swift */; }; A6CFB8D91F5024A500A42CC2 /* Error+NetworkingExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6CFB8D81F5024A500A42CC2 /* Error+NetworkingExtensions.swift */; }; B326804BA6CC8B8BB136A46A /* Pods_LeadKitAdditions_LeadKitAdditions_iOS_Extensions.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFD5627139CAB27705F75C07 /* Pods_LeadKitAdditions_LeadKitAdditions_iOS_Extensions.framework */; }; CAE698E81E968820000394B0 /* LeadKitAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = CAE698E61E968820000394B0 /* LeadKitAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -78,11 +85,19 @@ 01605ECA03749D49C27FA3DD /* Pods-LeadKitAdditions-LeadKitAdditions iOS Extensions.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LeadKitAdditions-LeadKitAdditions iOS Extensions.release.xcconfig"; path = "Pods/Target Support Files/Pods-LeadKitAdditions-LeadKitAdditions iOS Extensions/Pods-LeadKitAdditions-LeadKitAdditions iOS Extensions.release.xcconfig"; sourceTree = ""; }; 0ED4A1B793EAA73C9E95969F /* Pods_LeadKitAdditions_LeadKitAdditions_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LeadKitAdditions_LeadKitAdditions_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 49738551AC648B0AFA74E57F /* Pods-LeadKitAdditions-LeadKitAdditions iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LeadKitAdditions-LeadKitAdditions iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LeadKitAdditions-LeadKitAdditions iOS/Pods-LeadKitAdditions-LeadKitAdditions iOS.debug.xcconfig"; sourceTree = ""; }; + 67528BCE206E3CC6009F2525 /* iOS.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = iOS.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6760AF19207268EC00C2BB7E /* PinLayoutCell+DefaultImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PinLayoutCell+DefaultImplementation.swift"; sourceTree = ""; }; 67779CBB206986390098F024 /* BaseTextFieldViewEvents+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTextFieldViewEvents+Extensions.swift"; sourceTree = ""; }; 678D26A9206935B900B05B93 /* BiometricsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiometricsService.swift; sourceTree = ""; }; 67B4E6F1206945D200E233EA /* BaseTextFieldViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTextFieldViewModel.swift; sourceTree = ""; }; 67B4E6F5206945DC00E233EA /* OnlineValidationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnlineValidationResult.swift; sourceTree = ""; }; 67B4E6F8206945F900E233EA /* OnlineValidationState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnlineValidationState.swift; sourceTree = ""; }; + 67C2A41520724BBA000A5682 /* SeparatorTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorTableViewCell.swift; sourceTree = ""; }; + 67C2A41720724EA0000A5682 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = ""; }; + 67C2A41A20724F40000A5682 /* LabelCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelCellViewModel.swift; sourceTree = ""; }; + 67C2A41C20725359000A5682 /* LabelTableViewCell+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LabelTableViewCell+Extensions.swift"; sourceTree = ""; }; + 67CF05A9206E9880009A2AB9 /* PinLayoutCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinLayoutCell.swift; sourceTree = ""; }; + 67CF05AF206E99DF009A2AB9 /* PinLayoutTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinLayoutTableViewCell.swift; sourceTree = ""; }; 7B7F57C5E5275C4D8DC71992 /* Pods_LeadKitAdditions.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LeadKitAdditions.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9D549FA5A7579702358E07DF /* Pods-LeadKitAdditions-LeadKitAdditions iOS Extensions.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LeadKitAdditions-LeadKitAdditions iOS Extensions.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LeadKitAdditions-LeadKitAdditions iOS Extensions/Pods-LeadKitAdditions-LeadKitAdditions iOS Extensions.debug.xcconfig"; sourceTree = ""; }; A6CFB8D81F5024A500A42CC2 /* Error+NetworkingExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Error+NetworkingExtensions.swift"; sourceTree = ""; }; @@ -141,6 +156,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 67528BCB206E3C12009F2525 /* Cells */ = { + isa = PBXGroup; + children = ( + 67C2A41920724F32000A5682 /* LabelTableViewCell */, + 67CF05AF206E99DF009A2AB9 /* PinLayoutTableViewCell.swift */, + 67C2A41520724BBA000A5682 /* SeparatorTableViewCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; 67B4E6F0206945D200E233EA /* BaseTextFieldViewModel */ = { isa = PBXGroup; children = ( @@ -157,6 +182,32 @@ path = ValidationService; sourceTree = ""; }; + 67C2A41920724F32000A5682 /* LabelTableViewCell */ = { + isa = PBXGroup; + children = ( + 67C2A41720724EA0000A5682 /* LabelTableViewCell.swift */, + 67C2A41A20724F40000A5682 /* LabelCellViewModel.swift */, + ); + path = LabelTableViewCell; + sourceTree = ""; + }; + 67CF05A8206E986A009A2AB9 /* Cells */ = { + isa = PBXGroup; + children = ( + 67CF05A9206E9880009A2AB9 /* PinLayoutCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; + 67CF05AC206E9931009A2AB9 /* Cells */ = { + isa = PBXGroup; + children = ( + 67C2A41C20725359000A5682 /* LabelTableViewCell+Extensions.swift */, + 6760AF19207268EC00C2BB7E /* PinLayoutCell+DefaultImplementation.swift */, + ); + path = Cells; + sourceTree = ""; + }; A3117951840B8B7D2E7A8A80 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -174,6 +225,7 @@ F8A65FEC7C0EB4B93746E50F /* Pods */, CAE698E41E968820000394B0 /* Products */, CAE698E51E968820000394B0 /* Sources */, + 67528BCE206E3CC6009F2525 /* iOS.playground */, ); sourceTree = ""; }; @@ -205,6 +257,7 @@ ED0C33D21F2906EC00FAE9FD /* Classes */ = { isa = PBXGroup; children = ( + 67528BCB206E3C12009F2525 /* Cells */, 67B4E6F0206945D200E233EA /* BaseTextFieldViewModel */, ED0C33D31F2906EC00FAE9FD /* ApiResponse.swift */, ED0C33D41F2906EC00FAE9FD /* BaseDateFormatter.swift */, @@ -272,6 +325,7 @@ ED0C33E61F2906EC00FAE9FD /* Extensions */ = { isa = PBXGroup; children = ( + 67CF05AC206E9931009A2AB9 /* Cells */, ED0C33E71F2906EC00FAE9FD /* Observable+Extensions.swift */, ED0C33E81F2906EC00FAE9FD /* UIBarButtonItem+Extensions.swift */, ED0C33E91F2906EC00FAE9FD /* UserDefaults+UserService.swift */, @@ -284,6 +338,7 @@ ED0C33EA1F2906EC00FAE9FD /* Protocols */ = { isa = PBXGroup; children = ( + 67CF05A8206E986A009A2AB9 /* Cells */, 67B4E6F4206945DC00E233EA /* ValidationService */, ); path = Protocols; @@ -564,20 +619,26 @@ ED0C343F1F2906EC00FAE9FD /* ValidationError.swift in Sources */, ED0C342F1F2906EC00FAE9FD /* BaseUserService.swift in Sources */, 678D26AA206935B900B05B93 /* BiometricsService.swift in Sources */, + 67C2A41D20725359000A5682 /* LabelTableViewCell+Extensions.swift in Sources */, ED0C34411F2906EC00FAE9FD /* ValidationItem.swift in Sources */, ED0C341F1F2906EC00FAE9FD /* UIBarButtonItem+Extensions.swift in Sources */, + 67C2A41820724EA0000A5682 /* LabelTableViewCell.swift in Sources */, ED0C34091F2906EC00FAE9FD /* PassCodeConfiguration.swift in Sources */, EF5A43B1206E7A67003CED07 /* PassCodeDelayedDescription.swift in Sources */, ED0C341D1F2906EC00FAE9FD /* Observable+Extensions.swift in Sources */, + 67CF05B0206E99DF009A2AB9 /* PinLayoutTableViewCell.swift in Sources */, ED0C34191F2906EC00FAE9FD /* ApiErrorProtocol.swift in Sources */, ED0C34131F2906EC00FAE9FD /* BasePassCodeViewController.swift in Sources */, + 67C2A41620724BBA000A5682 /* SeparatorTableViewCell.swift in Sources */, ED0C342D1F2906EC00FAE9FD /* BasePassCodeService.swift in Sources */, 67B4E6F9206945F900E233EA /* OnlineValidationState.swift in Sources */, 67779CBC206986390098F024 /* BaseTextFieldViewEvents+Extensions.swift in Sources */, ED0C340D1F2906EC00FAE9FD /* PassCodeHolder.swift in Sources */, + 67CF05AA206E9880009A2AB9 /* PinLayoutCell.swift in Sources */, ED0C34031F2906EC00FAE9FD /* ApiResponse.swift in Sources */, A6CFB8D91F5024A500A42CC2 /* Error+NetworkingExtensions.swift in Sources */, ED0C34071F2906EC00FAE9FD /* LoadingBarButton.swift in Sources */, + 6760AF1A207268EC00C2BB7E /* PinLayoutCell+DefaultImplementation.swift in Sources */, ED0C34211F2906EC00FAE9FD /* UserDefaults+UserService.swift in Sources */, ED0C34391F2906EC00FAE9FD /* DefaultNetworkService+ActivityIndicator.swift in Sources */, ED0C34111F2906EC00FAE9FD /* PassCodeValidationResult.swift in Sources */, @@ -586,6 +647,7 @@ ED0C34331F2906EC00FAE9FD /* MaskFieldTextProxy.swift in Sources */, ED0C340B1F2906EC00FAE9FD /* PassCodeError.swift in Sources */, 67B4E6F6206945DD00E233EA /* OnlineValidationResult.swift in Sources */, + 67C2A41B20724F40000A5682 /* LabelCellViewModel.swift in Sources */, ED0C34151F2906EC00FAE9FD /* BasePassCodeViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Podfile b/Podfile index 1b5e5a4..045cb90 100644 --- a/Podfile +++ b/Podfile @@ -7,6 +7,7 @@ abstract_target 'LeadKitAdditions' do pod "InputMask", '3.0.0' pod "SwiftValidator", '5.0.0' pod "SwiftLint", '~> 0.25' + pod "PinLayout", '~> 1.6' inhibit_all_warnings! diff --git a/Podfile.lock b/Podfile.lock index c7f2047..af3a63b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -25,6 +25,7 @@ PODS: - RxSwift (~> 4.1) - SwiftDate (~> 4.5) - ObjectMapper (3.1.0) + - PinLayout (1.6.0) - RxAlamofire (4.1.0): - RxAlamofire/Core (= 4.1.0) - RxAlamofire/Core (4.1.0): @@ -43,19 +44,25 @@ DEPENDENCIES: - IDZSwiftCommonCrypto - InputMask (= 3.0.0) - KeychainAccess (= 3.1.0) - - LeadKit (~> 0.7.0) - - LeadKit/Core-iOS-Extension (~> 0.7.0) + - LeadKit (from `/Users/ivansmolin/Projects/tmp/LeadKit`) + - LeadKit/Core-iOS-Extension (from `/Users/ivansmolin/Projects/tmp/LeadKit`) + - PinLayout (~> 1.6) - SwiftLint (~> 0.25) - SwiftValidator (= 5.0.0) +EXTERNAL SOURCES: + LeadKit: + :path: /Users/ivansmolin/Projects/tmp/LeadKit + SPEC CHECKSUMS: Alamofire: 68d7d521118d49c615a8d2214d87cdf525599d30 CocoaLumberjack: 2e258a064cacc8eb9a2aca318e24d02a0a7fd56d IDZSwiftCommonCrypto: 4eef2c46e262dfbcbc1fd76365e066336680ad7d InputMask: 37c273bde6705187d80cf0b4240cb42ea92096c3 KeychainAccess: 94c5540b32eabf7bc32bfb976a268e8ea05fd6da - LeadKit: 741848fb41fda9f83fca9bbc2627a290b4881ab9 + LeadKit: 65b21acf728e87d15e497bd92eae37f22c8c95e5 ObjectMapper: 20505058f54e5c3ca69e1d6de9897d152a5369a6 + PinLayout: a2bbe9057d49a1e326b13dc4fe8c14751f8c8844 RxAlamofire: 96a2bff4694a1609bb59c57b53d99ea7a0ddc64a RxCocoa: d88ba0f1f6abf040011a9eb4b539324fc426843a RxSwift: e49536837d9901277638493ea537394d4b55f570 @@ -65,6 +72,6 @@ SPEC CHECKSUMS: TableKit: 61880e4c13ac0ba396a308fcb1ae48f6dec8b458 UIScrollView-InfiniteScroll: c132d6d5851daff229ab4a1060ccf70a05a051c9 -PODFILE CHECKSUM: b9c752a6fd2bd5c6ecdab991fac4af266941ddf1 +PODFILE CHECKSUM: 1cf6417e00d3d5db4e108436ce72412ad60d78cf COCOAPODS: 1.4.0 diff --git a/Sources/Classes/Cells/LabelTableViewCell/LabelCellViewModel.swift b/Sources/Classes/Cells/LabelTableViewCell/LabelCellViewModel.swift new file mode 100644 index 0000000..8451897 --- /dev/null +++ b/Sources/Classes/Cells/LabelTableViewCell/LabelCellViewModel.swift @@ -0,0 +1,56 @@ +// +// Copyright (c) 2018 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import UIKit.UIGeometry +import LeadKit + +/// Default view model for LabelTableViewCell. +open class LabelCellViewModel { + + public let viewText: ViewText + public let contentBackground: ViewBackground + public let contentInsets: UIEdgeInsets + public let labelInsets: UIEdgeInsets + public let separatorType: CellSeparatorType + + /// Memberwise initializer. + /// + /// - Parameters: + /// - viewText: View text to configure label. + /// - contentBackground: View background to configure background. + /// - contentInsets: Content insets to use for layout whole content. + /// - labelInsets: Label insets to use for layout label. + /// - separatorType: Separator type to use for separators. + public init(viewText: ViewText, + contentBackground: ViewBackground = .color(.clear), + contentInsets: UIEdgeInsets = .zero, + labelInsets: UIEdgeInsets = .zero, + separatorType: CellSeparatorType = .none) { + + self.viewText = viewText + self.contentBackground = contentBackground + self.contentInsets = contentInsets + self.labelInsets = labelInsets + self.separatorType = separatorType + } + +} diff --git a/Sources/Classes/Cells/LabelTableViewCell/LabelTableViewCell.swift b/Sources/Classes/Cells/LabelTableViewCell/LabelTableViewCell.swift new file mode 100644 index 0000000..2337c7d --- /dev/null +++ b/Sources/Classes/Cells/LabelTableViewCell/LabelTableViewCell.swift @@ -0,0 +1,141 @@ +// +// Copyright (c) 2018 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import UIKit +import LeadKit +import PinLayout + +/// Pin layout cell with separators, background and label. +open class LabelTableViewCell: SeparatorTableViewCell { + + private let label = UILabel(frame: .zero) + private let backgroundImageView = UIImageView(frame: .zero) + private let contentContainerView = UIView(frame: .zero) + + private var viewModel: LabelCellViewModel? + + // MARK: - PinLayoutTableViewCell + + override open func addViews() { + super.addViews() + + contentContainerView.addSubview(backgroundImageView) + contentContainerView.addSubview(label) + + contentView.addSubview(contentContainerView) + } + + override open func configureAppearance() { + super.configureAppearance() + + selectionStyle = .none + backgroundColor = .clear + + contentView.backgroundColor = .clear + + configureAppearance(of: label, backgroundImageView: backgroundImageView) + } + + override open func layout() { + let topSeparatorHeight = viewModel?.separatorType.topConfiguration?.totalHeight ?? 0 + let bottomSeparatorHeight = viewModel?.separatorType.bottomConfiguration?.totalHeight ?? 0 + + contentContainerView.pin + .top(topSeparatorHeight + contentInsets.top) + .horizontally(contentInsets) + .bottom(contentInsets.bottom + bottomSeparatorHeight) + .layout() + + label.pin + .top(labelInsets) + .horizontally(labelInsets) + .sizeToFit(.width) + .layout() + + backgroundImageView.pin + .all() + .layout() + + // bottom separator positioning after content size (height) calculation + super.layout() + } + + private var labelInsets: UIEdgeInsets { + return viewModel?.labelInsets ?? .zero + } + + private var contentInsets: UIEdgeInsets { + return viewModel?.contentInsets ?? .zero + } + + override open var contentHeight: CGFloat { + let selfContentHeight = contentInsets.top + + labelInsets.top + + label.frame.height + + labelInsets.bottom + + contentInsets.bottom + + return selfContentHeight + super.contentHeight + } + + // MARK: - Subclass methods to override + + /// Callback for label and background image view appearance configuration. + /// + /// - Parameters: + /// - label: Internal UILabel instance to configure. + /// - backgroundImageView: Internal UIImageView instance to configure. + open func configureAppearance(of label: UILabel, backgroundImageView: UIImageView) { + label.numberOfLines = 0 + } + + // MARK: - Configuration methods + + /// Convenient method for configuration cell with LabelCellViewModel. + /// + /// - Parameter viewModel: LabelCellViewModel instance. + public func configureLabelCell(with viewModel: LabelCellViewModel) { + self.viewModel = viewModel + + configureSeparator(with: viewModel.separatorType) + configureLabelText(with: viewModel.viewText) + configureContentBackground(with: viewModel.contentBackground) + + setNeedsLayout() + } + + /// Method for background configuration. + /// + /// - Parameter contentBackground: Content background to use as background. + public func configureContentBackground(with contentBackground: ViewBackground) { + contentBackground.configure(backgroundView: contentContainerView, + backgroundImageView: backgroundImageView) + } + + /// Method for text configuration. + /// + /// - Parameter viewText: View text to use as background. + public func configureLabelText(with viewText: ViewText) { + label.configure(with: viewText) + } + +} diff --git a/Sources/Classes/Cells/PinLayoutTableViewCell.swift b/Sources/Classes/Cells/PinLayoutTableViewCell.swift new file mode 100644 index 0000000..0b9ce7a --- /dev/null +++ b/Sources/Classes/Cells/PinLayoutTableViewCell.swift @@ -0,0 +1,104 @@ +// +// Copyright (c) 2018 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import UIKit +import PinLayout + +/// Cell that uses PinLayout. Contains methods that should be overriden in subclasses. +open class PinLayoutTableViewCell: UITableViewCell, PinLayoutCell { + + public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + initializeCell() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + initializeCell() + } + + open override func sizeThatFits(_ size: CGSize) -> CGSize { + return layout(with: size) + } + + open override func layoutSubviews() { + super.layoutSubviews() + + layout() + } + + // MARK: - PinLayoutCell + + open func initializeCell() { + addViews() + configureAppearance() + configureLayout() + } + + open func layout(with containerSize: CGSize) -> CGSize { + // 1) Set the contentView's width to the specified size parameter + contentView.pin.width(containerSize.width).layout() + + // 2) Layout the contentView's controls + layout() + + // 3) Returns a size that contains all controls + return CGSize(width: contentView.frame.width, + height: contentHeight) + } + + open func addViews() { + // override in subclass + + // move from _UISnapshotWindow superview in Playground + addSubview(contentView) + } + + open func configureAppearance() { + // override in subclass + } + + open func configureLayout() { + // override in subclass + } + + open func layout() { + // override in subclass + } + + open var contentHeight: CGFloat { + return contentView.subviewsMaxY + } + +} + +private extension UIView { + + var subviewsMaxY: CGFloat { + return subviews + .map { $0.frame.maxY } + .max() ?? frame.maxY + } + +} diff --git a/Sources/Classes/Cells/SeparatorTableViewCell.swift b/Sources/Classes/Cells/SeparatorTableViewCell.swift new file mode 100644 index 0000000..1f3dfe7 --- /dev/null +++ b/Sources/Classes/Cells/SeparatorTableViewCell.swift @@ -0,0 +1,104 @@ +// +// Copyright (c) 2018 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import UIKit +import LeadKit +import PinLayout + +/// Pin layout cell with top and bottom separators. +open class SeparatorTableViewCell: PinLayoutTableViewCell { + + private let topSeparator = UIView(frame: .zero) + private let bottomSeparator = UIView(frame: .zero) + + private var separatorType: CellSeparatorType = .none + + /// Configure separator with viewModel. + /// - parameter separatorType: type of separators. + public func configureSeparator(with separatorType: CellSeparatorType) { + self.separatorType = separatorType + + separatorType.configure(topSeparatorView: topSeparator, + bottomSeparatorView: bottomSeparator) + + setNeedsLayout() + } + + /// Move separator upward in hierarchy + public func bringSeparatorsToFront() { + contentView.bringSubview(toFront: topSeparator) + contentView.bringSubview(toFront: bottomSeparator) + } + + /// Move separator backward in hierarchy + public func sendSeparatorsToBack() { + contentView.sendSubview(toBack: topSeparator) + contentView.sendSubview(toBack: bottomSeparator) + } + + // MARK: - PinLayoutTableViewCell + + override open func addViews() { + super.addViews() + + contentView.addSubview(topSeparator) + contentView.addSubview(bottomSeparator) + } + + override open func layout() { + super.layout() + + if let topConfiguration = separatorType.topConfiguration { + topSeparator.pin + .top(topConfiguration.insets) + .horizontally(topConfiguration.insets) + .height(topConfiguration.height) + .layout() + } + + if let bottomConfiguration = separatorType.bottomConfiguration { + let topInset = contentHeight - (bottomConfiguration.height + bottomConfiguration.insets.bottom) + + bottomSeparator.pin + .top(topInset) + .horizontally(bottomConfiguration.insets) + .height(bottomConfiguration.height) + .layout() + } + } + + override open var contentHeight: CGFloat { + let topSeparatorHeight = separatorType.topConfiguration?.totalHeight ?? 0 + let bottomSeparatorHeight = separatorType.bottomConfiguration?.totalHeight ?? 0 + + return topSeparatorHeight + bottomSeparatorHeight + } + + // MARK: - UITableViewCell + + override open func prepareForReuse() { + super.prepareForReuse() + + configureSeparator(with: .none) + } + +} diff --git a/Sources/Extensions/Cells/LabelTableViewCell+Extensions.swift b/Sources/Extensions/Cells/LabelTableViewCell+Extensions.swift new file mode 100644 index 0000000..52fc105 --- /dev/null +++ b/Sources/Extensions/Cells/LabelTableViewCell+Extensions.swift @@ -0,0 +1,31 @@ +// +// Copyright (c) 2018 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import TableKit + +extension LabelTableViewCell: ConfigurableCell { + + public func configure(with viewModel: LabelCellViewModel) { + configureLabelCell(with: viewModel) + } + +} diff --git a/Sources/Extensions/Cells/PinLayoutCell+DefaultImplementation.swift b/Sources/Extensions/Cells/PinLayoutCell+DefaultImplementation.swift new file mode 100644 index 0000000..df5bb92 --- /dev/null +++ b/Sources/Extensions/Cells/PinLayoutCell+DefaultImplementation.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) 2018 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +public extension PinLayoutCell { + + func initializeCell() { + addViews() + configureAppearance() + configureLayout() + } + +} diff --git a/Sources/Protocols/Cells/PinLayoutCell.swift b/Sources/Protocols/Cells/PinLayoutCell.swift new file mode 100644 index 0000000..a0380ef --- /dev/null +++ b/Sources/Protocols/Cells/PinLayoutCell.swift @@ -0,0 +1,60 @@ +// +// Copyright (c) 2018 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import CoreGraphics.CGBase + +/// Protocol with methods for configuration and layout cell. +public protocol PinLayoutCell { + + /// Method is called when cell initialized from nib or from code. + func initializeCell() + + /// Method for adding subviews to content view. + /// + /// - Returns: Nothing. + func addViews() + + /// Method for cofiguring appearance of views. + /// + /// - Returns: Nothing. + func configureAppearance() + + /// Method for cofiguring layout and layout properties. + /// + /// - Returns: Nothing. + func configureLayout() + + /// Method is called during layout calls. + /// + /// - Returns: Nothing. + func layout() + + /// Method for calculating best-fitting cell size. + /// + /// - Parameter containerSize: The size for which the view should calculate its best-fitting size. + /// - Returns: Best-fitting size. + func layout(with containerSize: CGSize) -> CGSize + + /// Current content height. + var contentHeight: CGFloat { get } + +} diff --git a/iOS.playground/Contents.swift b/iOS.playground/Contents.swift new file mode 100644 index 0000000..938f62f --- /dev/null +++ b/iOS.playground/Contents.swift @@ -0,0 +1,4 @@ +import LeadKit +import LeadKitAdditions +import PlaygroundSupport + diff --git a/iOS.playground/Resources/common_global_all_rounded_background_radius_8@3x.png b/iOS.playground/Resources/common_global_all_rounded_background_radius_8@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5d83f19e63b02754136e2e26068f5d078c0dbb59 GIT binary patch literal 19479 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 literal 0 HcmV?d00001 diff --git a/iOS.playground/contents.xcplayground b/iOS.playground/contents.xcplayground new file mode 100644 index 0000000..9f5f2f4 --- /dev/null +++ b/iOS.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file