diff --git a/Example/FramezillaExample.xcodeproj/project.pbxproj b/Example/FramezillaExample.xcodeproj/project.pbxproj index 8d24668..3237ed2 100644 --- a/Example/FramezillaExample.xcodeproj/project.pbxproj +++ b/Example/FramezillaExample.xcodeproj/project.pbxproj @@ -146,7 +146,7 @@ attributes = { LastSwiftUpdateCheck = 0830; LastUpgradeCheck = 0900; - ORGANIZATIONNAME = "Nikita Ermolenko"; + ORGANIZATIONNAME = Rosberry; TargetAttributes = { 8442F94E1EC75C9C00B72551 = { CreatedOnToolsVersion = 8.3.2; @@ -159,6 +159,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); diff --git a/Example/FramezillaExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/FramezillaExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/FramezillaExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/FramezillaExample.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Example/FramezillaExample.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..bfcd7ed --- /dev/null +++ b/Example/FramezillaExample.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,10 @@ + + + + + FILEHEADER + +// Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. +// + + diff --git a/Example/FramezillaExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/FramezillaExample/Assets.xcassets/AppIcon.appiconset/Contents.json index 36d2c80..d8db8d6 100644 --- a/Example/FramezillaExample/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/FramezillaExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", @@ -30,6 +40,16 @@ "size" : "60x60", "scale" : "3x" }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, { "idiom" : "ipad", "size" : "29x29", @@ -59,6 +79,16 @@ "idiom" : "ipad", "size" : "76x76", "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { diff --git a/Framezilla.xcodeproj/project.pbxproj b/Framezilla.xcodeproj/project.pbxproj index c4b1cb3..8c4d4a1 100644 --- a/Framezilla.xcodeproj/project.pbxproj +++ b/Framezilla.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 8497C0111E59EB7700447E2F /* Number.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8497C0101E59EB7700447E2F /* Number.swift */; }; 8497C0131E59FA4B00447E2F /* Array+Stack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8497C0121E59FA4B00447E2F /* Array+Stack.swift */; }; 84EDCC241F9B3AB10091FAB9 /* MakerSafeAreaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EDCC231F9B3AB10091FAB9 /* MakerSafeAreaTests.swift */; }; + 85221C0022E99D4100EA9D3D /* OptionSet+ForEeach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85221BFF22E99D4100EA9D3D /* OptionSet+ForEeach.swift */; }; + 852FEEE322D473F200B79A6C /* ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852FEEE222D473F200B79A6C /* ExtensionsTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -69,6 +71,8 @@ 8497C0101E59EB7700447E2F /* Number.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Number.swift; sourceTree = ""; }; 8497C0121E59FA4B00447E2F /* Array+Stack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Stack.swift"; sourceTree = ""; }; 84EDCC231F9B3AB10091FAB9 /* MakerSafeAreaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakerSafeAreaTests.swift; sourceTree = ""; }; + 85221BFF22E99D4100EA9D3D /* OptionSet+ForEeach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OptionSet+ForEeach.swift"; sourceTree = ""; }; + 852FEEE222D473F200B79A6C /* ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -125,6 +129,7 @@ 8497C0121E59FA4B00447E2F /* Array+Stack.swift */, 842BB7FB1F0638E9000D1CFF /* Parameters.swift */, 844BE8F220457634004C19A8 /* Maker+Unavailable.swift */, + 85221BFF22E99D4100EA9D3D /* OptionSet+ForEeach.swift */, ); path = Sources; sourceTree = ""; @@ -144,6 +149,7 @@ 8442F93D1EC75A8500B72551 /* StateTests.swift */, 845108061EE2F5BC006DC1C8 /* ScrollViewTests.swift */, 841192DC1FF205DC00AB255D /* ContainerTests.swift */, + 852FEEE222D473F200B79A6C /* ExtensionsTests.swift */, ); path = Tests; sourceTree = ""; @@ -206,7 +212,8 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0800; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 1000; + ORGANIZATIONNAME = Rosberry; TargetAttributes = { 115972131D8450F500BC5C20 = { CreatedOnToolsVersion = 8.0; @@ -225,6 +232,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 11FB41261D844CD800700A40; @@ -281,6 +289,7 @@ 8442F9451EC75A8500B72551 /* StateTests.swift in Sources */, 841192DD1FF205DC00AB255D /* ContainerTests.swift in Sources */, 8442F9411EC75A8500B72551 /* MakerCenterTests.swift in Sources */, + 852FEEE322D473F200B79A6C /* ExtensionsTests.swift in Sources */, 8442F9421EC75A8500B72551 /* MakerStackTests.swift in Sources */, 8442F9401EC75A8500B72551 /* MakerBothSideRelationsTests.swift in Sources */, 845108071EE2F5BC006DC1C8 /* ScrollViewTests.swift in Sources */, @@ -296,6 +305,7 @@ files = ( 11FB41461D844F8F00700A40 /* Maker+Configurations.swift in Sources */, 11FB41451D844F8F00700A40 /* Maker.swift in Sources */, + 85221C0022E99D4100EA9D3D /* OptionSet+ForEeach.swift in Sources */, 8497C0111E59EB7700447E2F /* Number.swift in Sources */, 11FB41471D844F8F00700A40 /* MakerHelper.swift in Sources */, 8497C0131E59FA4B00447E2F /* Array+Stack.swift in Sources */, @@ -433,11 +443,13 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; @@ -464,11 +476,13 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; @@ -544,7 +558,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -602,7 +616,7 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; diff --git a/Framezilla.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Framezilla.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Framezilla.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Framezilla.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Framezilla.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..bfcd7ed --- /dev/null +++ b/Framezilla.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,10 @@ + + + + + FILEHEADER + +// Copyright © ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved. +// + + diff --git a/Framezilla.xcodeproj/xcshareddata/xcschemes/Framezilla iOS.xcscheme b/Framezilla.xcodeproj/xcshareddata/xcschemes/Framezilla iOS.xcscheme index c462db0..121fd35 100644 --- a/Framezilla.xcodeproj/xcshareddata/xcschemes/Framezilla iOS.xcscheme +++ b/Framezilla.xcodeproj/xcshareddata/xcschemes/Framezilla iOS.xcscheme @@ -26,9 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" - shouldUseLaunchSchemeArgsEnv = "YES" - codeCoverageEnabled = "YES"> + codeCoverageEnabled = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -57,7 +56,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Framezilla.xcodeproj/xcshareddata/xcschemes/FramezillaTests.xcscheme b/Framezilla.xcodeproj/xcshareddata/xcschemes/FramezillaTests.xcscheme new file mode 100644 index 0000000..4f28aaa --- /dev/null +++ b/Framezilla.xcodeproj/xcshareddata/xcschemes/FramezillaTests.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 0cb4a0d..566a2f5 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,21 @@ view.configureFrame { maker in ``` the second method has optional parameters, so ```maker.edges(top: 5, left: 5, bottom: 5)``` also works correct, but does not create ```right``` relation, that in some cases is very useful. +Also if you have `UIEdgeInsets` property and you want to use cup of them you can use + +```swift +let insets: UIEdgeInsets = .init(top: 15, left: 10, bottom: 20, right: 10) +view.configureFrame { maker in + maker.edges(insets: insets, sides: [.left, .right]) +} +``` +or +```swift +view.configureFrame { maker in + maker.edges(insets: insets, sides: .horizontal) +} +``` + ## Side relations (Top, left, bottom, right) You can create edge relation, as shown above, but only use side relations. diff --git a/Sources/Array+Stack.swift b/Sources/Array+Stack.swift index f953b06..8a68d3a 100644 --- a/Sources/Array+Stack.swift +++ b/Sources/Array+Stack.swift @@ -11,7 +11,7 @@ public enum StackAxis: Int { case vertical } -public extension Collection where Iterator.Element: UIView, Self.Index == Int, Self.IndexDistance == Int { +public extension Collection where Iterator.Element: UIView, Self.Index == Int { /// Arranges views in the order of list along a vertical or horizontal axis, with spacing property. /// diff --git a/Sources/Maker.swift b/Sources/Maker.swift index 79dcb17..6623a8a 100644 --- a/Sources/Maker.swift +++ b/Sources/Maker.swift @@ -24,6 +24,26 @@ public var nui_safeArea: SafeArea { return SafeArea() } +/// Used for choosing which side should be used for frame configuration from UIEdgeInsets. + +public struct Sides: OptionSet { + public let rawValue: Int + + public static let top: Sides = .init(rawValue: 1 << 0) + public static let bottom: Sides = .init(rawValue: 1 << 1) + public static let left: Sides = .init(rawValue: 1 << 2) + public static let right: Sides = .init(rawValue: 1 << 3) + + public static let vertical: Sides = [.top, .bottom] + public static let horizontal: Sides = [.left, .right] + + public static let all: Sides = [.vertical, .horizontal] + + public init(rawValue: Int) { + self.rawValue = rawValue + } +} + public final class Maker { typealias HandlerType = () -> Void @@ -547,24 +567,28 @@ public final class Maker { /// /// - parameter insets: The insets for setting relations for superview. /// + /// - parameter sides: The sides which will inculed from edge insets to setting relations. + /// /// - returns: `Maker` instance for chaining relations. - - @discardableResult public func edges(insets: UIEdgeInsets) -> Maker { - guard let superview = view.superview else { - assertionFailure("Can not create edge relations without superview.") - return self - } - - let handler = { [unowned self, unowned superview] in - let width = superview.bounds.width - (insets.left + insets.right) - let height = superview.bounds.height - (insets.top + insets.bottom) - let frame = CGRect(x: insets.left, y: insets.top, width: width, height: height) - self.newRect = frame + + @discardableResult public func edges(insets: UIEdgeInsets, sides: Sides = .all) -> Maker { + sides.forEach { side in + switch side { + case .bottom: + bottom(inset: insets.bottom) + case .left: + left(inset: insets.left) + case .right: + right(inset: insets.right) + case .top: + top(inset: insets.top) + default: + return + } } - handlers.append((.middle, handler)) return self } - + /// Creates bottom relation to superview. /// /// Use this method when you want to join bottom side of current view with bottom side of superview. diff --git a/Sources/OptionSet+ForEeach.swift b/Sources/OptionSet+ForEeach.swift new file mode 100644 index 0000000..4e6a62c --- /dev/null +++ b/Sources/OptionSet+ForEeach.swift @@ -0,0 +1,16 @@ +// +// Copyright © 2019 Rosberry. All rights reserved. +// + +extension OptionSet where RawValue: BinaryInteger, Element == Self { + public func forEach(_ body: (Self) -> Void) { + var i = 0 + while i < rawValue.bitWidth { + let option = Self.init(rawValue: 1 << i) + if contains(option) { + body(option) + } + i += 1 + } + } +} diff --git a/Tests/ExtensionsTests.swift b/Tests/ExtensionsTests.swift new file mode 100644 index 0000000..b93be2c --- /dev/null +++ b/Tests/ExtensionsTests.swift @@ -0,0 +1,64 @@ +// +// Copyright © 2019 Rosberry. All rights reserved. +// + +import XCTest + +final class ExtensionsTests: BaseTest { + + private struct TestOptionSet: OptionSet, CustomStringConvertible { + let rawValue: Int + + static let option1: TestOptionSet = .init(rawValue: 1 << 0) + static let option2: TestOptionSet = .init(rawValue: 1 << 1) + static let option3: TestOptionSet = .init(rawValue: 1 << 2) + + static let combinedOption: TestOptionSet = [.option1, .option3] + + static let all: TestOptionSet = [.combinedOption, .option2] + + init(rawValue: Int) { + self.rawValue = rawValue + } + + public var description: String { + switch self { + case .option1: + return "option1" + case .option2: + return "option2" + case .option3: + return "option3" + case .combinedOption: + return "combinedOption" + case .all: + return "all" + default: + return "default" + } + } + } + + func testOptionSetForEach() { + let optionSets: [TestOptionSet] = [.all, .option1, .combinedOption] + let allOptionsCount = 3 + let combinedOptionsCount = 2 + optionSets.forEach { options in + var iterationsCount = 0 + options.forEach({ option in + iterationsCount += 1 + XCTAssert(options.contains(option)) + }) + switch options { + case .all: + XCTAssertEqual(iterationsCount, allOptionsCount, "There's more iterations than needed") + case .option1: + XCTAssertEqual(iterationsCount, 1, "There's more iterations than needed") + case .combinedOption: + XCTAssertEqual(iterationsCount, combinedOptionsCount, "There's more iterations than needed") + default: + XCTAssertNil(nil, "Option not exist or not handled") + } + } + } +} diff --git a/Tests/MakerTests.swift b/Tests/MakerTests.swift index 95a3368..137c12f 100644 --- a/Tests/MakerTests.swift +++ b/Tests/MakerTests.swift @@ -7,6 +7,7 @@ // import XCTest +import Framezilla class MakerTests: BaseTest { @@ -361,7 +362,80 @@ class MakerTests: BaseTest { } XCTAssertEqual(testingView.frame, CGRect(x: 20, y: 10, width: 420, height: 450)) } - + + func testThatCorrectlyConfiguresSliceOf_edge_insets_toSuperview() { + // There're unordered array of available side option sets which will tested. + let sidesOptions: [Sides] = [.all, .horizontal, .vertical, .left, .right, .bottom, .top].shuffled() + // All options which will test will saved in this property, for check which option was already done. + var currentSideOptionSet: Sides = [] + // Default frame properties which will modified like it should be and will compare with current frame late. + var x: CGFloat = 0 + var y: CGFloat = 0 + var width: CGFloat = testingView.frame.width + var height: CGFloat = testingView.frame.height + + sidesOptions.forEach { sides in + // Run loop for each option included in option. For example option `horizontal` include `left` and `right`. + sides.forEach { side in + // Save current option which will be tested. + currentSideOptionSet.insert(side) + // Configure frame with some insets. + let insets: UIEdgeInsets = .init(top: 20, left: 10, bottom: 15, right: 10) + testingView.configureFrame { maker in + maker.edges(insets: insets, sides: currentSideOptionSet) + } + // Update default frame properties like it should be configured in best scenario + switch side { + case .bottom: + // If configure bottom with already configured top inset, so Y position of view should be equal to + // top inset over the superview and height should be calculated too + if currentSideOptionSet.contains(.top) { + y = insets.top + height = mainView.frame.height - insets.top - insets.bottom + } + // Y position wihout configured top inset before should be equal to distance between main view maxY + // and testing view subject to bottom inset + else { + y = mainView.frame.height - insets.bottom - testingView.frame.height + } + case .left: + x = insets.left + // If configure left with already configured right inset, so X position shouldn't be changed + // but width should be calculated constrained by left and right insets main view width + if currentSideOptionSet.contains(.right) { + width = mainView.frame.width - insets.left - insets.right + } + case .top: + y = insets.top + // If configure top with already configured bottom inset, so Y position shouldn't be changed + // but height should be calculated constrained by top and bottom insets main view height + if currentSideOptionSet.contains(.bottom) { + height = mainView.frame.height - insets.top - insets.bottom + } + case .right: + // If configure right with already configured left inset, so X position of view should be equal to + // left inset over the superview and width should be calculated too + if currentSideOptionSet.contains(.left) { + x = insets.left + width = mainView.frame.width - insets.left - insets.right + } + // X position wihout configured left inset before should be equal to distance between main view maxX + // and testing view subject to right inset + else { + x = mainView.frame.width - insets.right - testingView.frame.width + } + default: + XCTAssertNil(nil, "Side option not exist or not handled") + } + // Compare configured view frame with absolute frame + XCTAssertEqual(testingView.frame, + CGRect(x: x, y: y, width: width, height: height), + "Actual view frame not equal to absolute frame") + } + } + + } + func testThatCorrectlyConfigures_edge_toSuperview() { testingView.configureFrame { maker in @@ -377,14 +451,14 @@ class MakerTests: BaseTest { let view1 = UIView(frame: CGRect(x: 50, y: 50, width: 50, height: 50)) let view2 = UIView(frame: CGRect(x: 70, y: 70, width: 50, height: 50)) - let containet = UIView() - containet.addSubview(view1) - containet.addSubview(view2) + let containerView = UIView() + containerView.addSubview(view1) + containerView.addSubview(view2) - containet.configureFrame { maker in - maker._container() + containerView.configureFrame { maker in + maker.container() } - XCTAssertEqual(containet.frame, CGRect(x: 0, y: 0, width: 120, height: 120)) + XCTAssertEqual(containerView.frame, CGRect(x: 0, y: 0, width: 120, height: 120)) } /* sizeToFit */