diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..ad0d471 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,50 @@ +version: 2 + +workflows: + version: 2 + main: + jobs: + - hold: + type: approval + - build: + requires: [hold] + +jobs: + build: + requires: hold + # Specify the Xcode version to use + macos: + xcode: "10.1.0" + + steps: + - checkout + + - attach_workspace: + at: . + + - run: + name: Build and run tests + command: fastlane scan + environment: + SCAN_DEVICE: iPhone 7 + SCAN_SCHEME: SimplePagedViewFrameworkTests + SCAN_DERIVED_DATA_PATH: build/DerivedData + SCAN_OUTPUT_DIRECTORY: build/Output + + - persist_to_workspace: + root: . + paths: + - 'build' + + # Collect XML test results data to show in the UI, + # and save the same XML files under test-results folder + # in the Artifacts tab + - store_test_results: + path: test_output/report.xml + - store_artifacts: + path: /tmp/test-results + destination: scan-test-results + - store_artifacts: + path: ~/Library/Logs/scan + destination: scan-logs + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b0d62b --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bdb196e --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2018 Air Computing Inc + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1eeaf1 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# SimplePagedView + +SimplePagedView is an iOS component that makes it as easy as possible to set up a page view for things like onboarding or presenting information. + +![simulator screen shot - iphone 8 - 2018-12-12 at 12 54 46](https://user-images.githubusercontent.com/5561501/49899079-87bbda80-fe0f-11e8-82ec-ea523e6cd42c.png) + +## Installation + +```ruby +pod 'SimplePagedView' +``` + +## Usage + +```swift +// Create a PagedViewController by providing it with a view for each page you'd like it to contain +let pagedViewController = PagedViewController(with: + LogoView(presenter: welcomePresenter), + CardPageView(image: ThemeManager.Images.welcomeTourSlide1, + subtitle: "Complete and resolve tasks on the go"), + CardPageView(image: ThemeManager.Images.welcomeTourSlide2, + subtitle: "Comment on tasks and conversations"), + CardPageView(image: ThemeManager.Images.welcomeTourSlide3, + subtitle: "Add files and pictures in seconds") +) + +// Add the pagedViewController as a child view controller +self.add(pagedViewController) { (childView) -> [NSLayoutConstraint] in + // Return an array of constraints to apply to the paged view + return [ + childView.topAnchor.constraint(equalTo: view.readableContentGuide.topAnchor), + childView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), + childView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), + childView.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor) + ] +} +``` diff --git a/SimplePagedView.podspec b/SimplePagedView.podspec new file mode 100644 index 0000000..19ac09c --- /dev/null +++ b/SimplePagedView.podspec @@ -0,0 +1,27 @@ +Pod::Spec.new do |s| + + s.name = "SimplePagedView" + s.version = "0.0.2" + s.summary = "A PageViewController replacement built to be as simple as possible" + + s.description = <<-DESC +A PageViewController replacement built to be as simple as possible to use. Supports easy insertion of views and the classic page dots. Also supports many customization points alongside reasonable defaults. + DESC + + s.homepage = "http://github.com/redbooth/SimplePagedView" + + s.license = "MIT" + + s.author = { "Alex Reilly" => "alexander.r.reilly@gmail.com" } + s.social_media_url = "https://twitter.com/TheWisestFools" + + s.platform = :ios, "12.0" + + + s.source = { :git => "https://github.com/redbooth/SimplePagedView.git", :tag => s.version } + + s.source_files = "SimplePagedViewFramework/**/*{swift}" + + s.swift_version = "4.2" + +end diff --git a/SimplePagedViewFramework.xcodeproj/project.pbxproj b/SimplePagedViewFramework.xcodeproj/project.pbxproj index b9bccc9..8b22009 100644 --- a/SimplePagedViewFramework.xcodeproj/project.pbxproj +++ b/SimplePagedViewFramework.xcodeproj/project.pbxproj @@ -9,7 +9,12 @@ /* Begin PBXBuildFile section */ B2C6870F21C191F2000031AB /* SimplePagedViewFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2C6870521C191F2000031AB /* SimplePagedViewFramework.framework */; }; B2C6871421C191F2000031AB /* SimplePagedViewFrameworkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C6871321C191F2000031AB /* SimplePagedViewFrameworkTests.swift */; }; - B2C6871621C191F2000031AB /* SimplePagedViewFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = B2C6870821C191F2000031AB /* SimplePagedViewFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B2C6872821C19218000031AB /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C6872021C19217000031AB /* NSLayoutConstraint.swift */; }; + B2C6872921C19218000031AB /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C6872121C19217000031AB /* UIImage.swift */; }; + B2C6872B21C19218000031AB /* ExternallyInteractiveUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C6872421C19217000031AB /* ExternallyInteractiveUIView.swift */; }; + B2C6872C21C19218000031AB /* SimplePagedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C6872521C19218000031AB /* SimplePagedView.swift */; }; + B2C6873121C1AE10000031AB /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C6873021C1AE10000031AB /* UIViewController.swift */; }; + B2C6898421C342D5000031AB /* PageDotsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C6898321C342D5000031AB /* PageDotsView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -24,11 +29,16 @@ /* Begin PBXFileReference section */ B2C6870521C191F2000031AB /* SimplePagedViewFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimplePagedViewFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B2C6870821C191F2000031AB /* SimplePagedViewFramework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimplePagedViewFramework.h; sourceTree = ""; }; B2C6870921C191F2000031AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B2C6870E21C191F2000031AB /* SimplePagedViewFrameworkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimplePagedViewFrameworkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B2C6871321C191F2000031AB /* SimplePagedViewFrameworkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplePagedViewFrameworkTests.swift; sourceTree = ""; }; B2C6871521C191F2000031AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B2C6872021C19217000031AB /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = ""; }; + B2C6872121C19217000031AB /* UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; + B2C6872421C19217000031AB /* ExternallyInteractiveUIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExternallyInteractiveUIView.swift; sourceTree = ""; }; + B2C6872521C19218000031AB /* SimplePagedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimplePagedView.swift; sourceTree = ""; }; + B2C6873021C1AE10000031AB /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; + B2C6898321C342D5000031AB /* PageDotsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageDotsView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -71,7 +81,7 @@ B2C6870721C191F2000031AB /* SimplePagedViewFramework */ = { isa = PBXGroup; children = ( - B2C6870821C191F2000031AB /* SimplePagedViewFramework.h */, + B2C6872F21C1A8C8000031AB /* Source */, B2C6870921C191F2000031AB /* Info.plist */, ); path = SimplePagedViewFramework; @@ -86,6 +96,43 @@ path = SimplePagedViewFrameworkTests; sourceTree = ""; }; + B2C6871F21C19217000031AB /* Extensions */ = { + isa = PBXGroup; + children = ( + B2C6872021C19217000031AB /* NSLayoutConstraint.swift */, + B2C6872121C19217000031AB /* UIImage.swift */, + B2C6873021C1AE10000031AB /* UIViewController.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + B2C6872321C19217000031AB /* Helpers */ = { + isa = PBXGroup; + children = ( + B2C6872421C19217000031AB /* ExternallyInteractiveUIView.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + B2C6872F21C1A8C8000031AB /* Source */ = { + isa = PBXGroup; + children = ( + B2C6872521C19218000031AB /* SimplePagedView.swift */, + B2C6898221C342B6000031AB /* Views */, + B2C6872321C19217000031AB /* Helpers */, + B2C6871F21C19217000031AB /* Extensions */, + ); + path = Source; + sourceTree = ""; + }; + B2C6898221C342B6000031AB /* Views */ = { + isa = PBXGroup; + children = ( + B2C6898321C342D5000031AB /* PageDotsView.swift */, + ); + path = Views; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -93,7 +140,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - B2C6871621C191F2000031AB /* SimplePagedViewFramework.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -148,6 +194,7 @@ TargetAttributes = { B2C6870421C191F2000031AB = { CreatedOnToolsVersion = 10.1; + LastSwiftMigration = 1010; }; B2C6870D21C191F2000031AB = { CreatedOnToolsVersion = 10.1; @@ -194,6 +241,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B2C6872821C19218000031AB /* NSLayoutConstraint.swift in Sources */, + B2C6898421C342D5000031AB /* PageDotsView.swift in Sources */, + B2C6872B21C19218000031AB /* ExternallyInteractiveUIView.swift in Sources */, + B2C6872C21C19218000031AB /* SimplePagedView.swift in Sources */, + B2C6872921C19218000031AB /* UIImage.swift in Sources */, + B2C6873121C1AE10000031AB /* UIViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -268,7 +321,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -326,7 +379,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -341,6 +394,7 @@ B2C6871A21C191F2000031AB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; @@ -349,6 +403,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SimplePagedViewFramework/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -357,6 +412,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.twof.SimplePagedViewFramework; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -365,6 +421,7 @@ B2C6871B21C191F2000031AB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; @@ -373,6 +430,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SimplePagedViewFramework/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -389,6 +447,7 @@ B2C6871D21C191F2000031AB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = SimplePagedViewFrameworkTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -406,6 +465,7 @@ B2C6871E21C191F2000031AB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = SimplePagedViewFrameworkTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/SimplePagedViewFramework.xcodeproj/xcshareddata/xcschemes/SimplePagedViewFramework.xcscheme b/SimplePagedViewFramework.xcodeproj/xcshareddata/xcschemes/SimplePagedViewFramework.xcscheme new file mode 100644 index 0000000..3147bd9 --- /dev/null +++ b/SimplePagedViewFramework.xcodeproj/xcshareddata/xcschemes/SimplePagedViewFramework.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SimplePagedViewFramework.xcodeproj/xcshareddata/xcschemes/SimplePagedViewFrameworkTests.xcscheme b/SimplePagedViewFramework.xcodeproj/xcshareddata/xcschemes/SimplePagedViewFrameworkTests.xcscheme new file mode 100644 index 0000000..6816af3 --- /dev/null +++ b/SimplePagedViewFramework.xcodeproj/xcshareddata/xcschemes/SimplePagedViewFrameworkTests.xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SimplePagedViewFramework.xcodeproj/xcuserdata/newuser.xcuserdatad/xcschemes/xcschememanagement.plist b/SimplePagedViewFramework.xcodeproj/xcuserdata/newuser.xcuserdatad/xcschemes/xcschememanagement.plist index 7d04b0d..57053f5 100644 --- a/SimplePagedViewFramework.xcodeproj/xcuserdata/newuser.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/SimplePagedViewFramework.xcodeproj/xcuserdata/newuser.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,6 +9,24 @@ orderHint 0 + SimplePagedViewFrameworkTests.xcscheme_^#shared#^_ + + orderHint + 1 + + + SuppressBuildableAutocreation + + B2C6870421C191F2000031AB + + primary + + + B2C6870D21C191F2000031AB + + primary + + diff --git a/SimplePagedViewFramework/SimplePagedViewFramework.h b/SimplePagedViewFramework/SimplePagedViewFramework.h deleted file mode 100644 index 7b2436c..0000000 --- a/SimplePagedViewFramework/SimplePagedViewFramework.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// SimplePagedViewFramework.h -// SimplePagedViewFramework -// -// Created by New User on 12/12/18. -// Copyright © 2018 twof. All rights reserved. -// - -#import - -//! Project version number for SimplePagedViewFramework. -FOUNDATION_EXPORT double SimplePagedViewFrameworkVersionNumber; - -//! Project version string for SimplePagedViewFramework. -FOUNDATION_EXPORT const unsigned char SimplePagedViewFrameworkVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/SimplePagedViewFramework/Source/Extensions/NSLayoutConstraint.swift b/SimplePagedViewFramework/Source/Extensions/NSLayoutConstraint.swift new file mode 100644 index 0000000..a02ca88 --- /dev/null +++ b/SimplePagedViewFramework/Source/Extensions/NSLayoutConstraint.swift @@ -0,0 +1,13 @@ +import UIKit + +public extension NSLayoutConstraint { + + /// Convenience method that activates each constraint in the list of arrays, in the same manner as setting active=true. This is often more efficient than activating each constraint individually. + /// + /// - Parameter constraintsList: Set of typically related constraints + @available(iOS 8.0, *) + class func activate(_ constraintsList: [NSLayoutConstraint]...) { + let constraints = Array(constraintsList.joined()) + NSLayoutConstraint.activate(constraints) + } +} diff --git a/SimplePagedViewFramework/Source/Extensions/UIImage.swift b/SimplePagedViewFramework/Source/Extensions/UIImage.swift new file mode 100644 index 0000000..a6593be --- /dev/null +++ b/SimplePagedViewFramework/Source/Extensions/UIImage.swift @@ -0,0 +1,32 @@ +import Foundation +import UIKit + +public extension UIImage { + + /// Creates a tinted copy of a template asset + /// + /// - Parameter color: The color to set the image to + /// - Returns: A tinted copy of self + func tint(with color: UIColor) -> UIImage { + guard let cgImage = cgImage else { + return self + } + UIGraphicsBeginImageContextWithOptions(size, false, scale) + guard let context = UIGraphicsGetCurrentContext() else { + UIGraphicsEndImageContext() + return self + } + color.setFill() + context.translateBy(x: 0, y: size.height) + context.scaleBy(x: 1.0, y: -1.0) + context.setBlendMode(.normal) + + let rect = CGRect(origin: .zero, size: size) + context.clip(to: rect, mask: cgImage) + context.fill(rect) + + let result = UIGraphicsGetImageFromCurrentImageContext() ?? self + UIGraphicsEndImageContext() + return result + } +} diff --git a/SimplePagedViewFramework/Source/Extensions/UIViewController.swift b/SimplePagedViewFramework/Source/Extensions/UIViewController.swift new file mode 100644 index 0000000..054c589 --- /dev/null +++ b/SimplePagedViewFramework/Source/Extensions/UIViewController.swift @@ -0,0 +1,31 @@ +import UIKit + +extension UIViewController { + /// Adds a child ViewController + func add(_ child: UIViewController) { + addChild(child) + view.addSubview(child.view) + child.didMove(toParent: self) + } + + /// Adds a subViewController and constraints to self + func add( + _ childViewController: UIViewController, + constraints: (UIView) -> [NSLayoutConstraint] + ) { + addChild(childViewController) + view.addSubview(childViewController.view) + childViewController.view.translatesAutoresizingMaskIntoConstraints = false + childViewController.didMove(toParent: self) + NSLayoutConstraint.activate(constraints(childViewController.view)) + } + + /// Removes a child ViewController + func removeFromParent() { + guard parent != nil else { return } + + willMove(toParent: nil) + removeFromParent() + view.removeFromSuperview() + } +} diff --git a/SimplePagedViewFramework/Source/Helpers/ExternallyInteractiveUIView.swift b/SimplePagedViewFramework/Source/Helpers/ExternallyInteractiveUIView.swift new file mode 100644 index 0000000..887b21a --- /dev/null +++ b/SimplePagedViewFramework/Source/Helpers/ExternallyInteractiveUIView.swift @@ -0,0 +1,17 @@ +import UIKit + +/// Gestures won't be recognized on subviews that are outside the bounds of a view +/// This subclass and override remedies that. +/// We're using it because there are situations where we want the page +/// indicator to be outside the page view +public class ExternallyInteractiveUIView: UIView { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard !clipsToBounds && !isHidden && alpha > 0 else { return nil } + for member in subviews.reversed() { + let subPoint = member.convert(point, from: self) + guard let result = member.hitTest(subPoint, with: event) else { continue } + return result + } + return nil + } +} diff --git a/SimplePagedViewFramework/Source/SimplePagedView.swift b/SimplePagedViewFramework/Source/SimplePagedView.swift new file mode 100644 index 0000000..1c7a1e1 --- /dev/null +++ b/SimplePagedViewFramework/Source/SimplePagedView.swift @@ -0,0 +1,327 @@ +import UIKit + +public class SimplePagedView: UIViewController { + + // MARK: - Properties + private static let defaultPageControlConstraints: (SimplePagedView) -> ([NSLayoutConstraint]) + = { (pagedViewController: SimplePagedView) in + return [ + pagedViewController.pageControl.bottomAnchor.constraint( + equalTo: pagedViewController.scrollView.bottomAnchor, + constant: Constants.pageControllerSpacing + ), + pagedViewController.pageControl.centerXAnchor.constraint( + equalTo: pagedViewController.view.centerXAnchor + ), + pagedViewController.pageControl.leadingAnchor.constraint( + equalTo: pagedViewController.scrollView.leadingAnchor + ), + pagedViewController.pageControl.trailingAnchor.constraint( + equalTo: pagedViewController.scrollView.trailingAnchor + ) + ] + } + + private enum Constants { + static let startingPage = 0 + static let pageControllerSpacing: CGFloat = -10 + } + + fileprivate var scrollContentView: UIView = { + var scrollingView = UIView() + scrollingView.translatesAutoresizingMaskIntoConstraints = false + return scrollingView + }() + fileprivate var innerPages: [UIView]! + fileprivate let pageControlConstraints: (SimplePagedView) -> ([NSLayoutConstraint]) + fileprivate let initialPage: Int + fileprivate var didInit = false + fileprivate let dotSize: CGFloat + + + /// Can be defined in order to trigger an action when pages are switched. Pages are 0 indexed. + public var didSwitchPages: ((Int) -> Void)? + /// Can be set to allow or disallow user interaction with the page dot indicators. Defaults to false. + public var pageIndicatorIsInteractive: Bool = false + /// The last dot can in the page indicator can be replaced with an image by setting this property + public var lastPageIndicator: UIImageView? + + public var scrollView: UIScrollView = { + var scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.isPagingEnabled = true + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + scrollView.bounces = false + scrollView.alwaysBounceHorizontal = false + return scrollView + }() + public var pageControl: UIPageControl = { + var pageControl = UIPageControl() + pageControl.translatesAutoresizingMaskIntoConstraints = false + pageControl.currentPage = Constants.startingPage + pageControl.pageIndicatorTintColor = #colorLiteral(red: 0.6980392157, green: 0.6980392157, blue: 0.6980392157, alpha: 1) + pageControl.currentPageIndicatorTintColor = #colorLiteral(red: 0.937254902, green: 0.2392156863, blue: 0.3098039216, alpha: 1) + pageControl.isUserInteractionEnabled = true + return pageControl + }() { + didSet { + self.viewDidLayoutSubviews() + } + } + + init( + indicatorColor: UIColor = .red, + pageControlBackgroundColor: UIColor = .clear, + initialPage: Int = 0, + dotSize: CGFloat = 7, + pageControlConstraints: @escaping (SimplePagedView) -> ([NSLayoutConstraint]) + = SimplePagedView.defaultPageControlConstraints, + with views: UIView... + ) { + self.pageControlConstraints = pageControlConstraints + self.initialPage = initialPage + self.dotSize = dotSize + super.init(nibName: nil, bundle: nil) + self.innerPages = setupInnerPages(for: views) + self.pageControl.currentPageIndicatorTintColor = indicatorColor + self.pageControl.backgroundColor = pageControlBackgroundColor + } + + init( + indicatorColor: UIColor = .red, + pageControlBackgroundColor: UIColor = .clear, + initialPage: Int = 0, + dotSize: CGFloat = 7, + pageControlConstraints: @escaping (SimplePagedView) -> ([NSLayoutConstraint]) + = SimplePagedView.defaultPageControlConstraints, + with views: [UIView] + ) { + self.pageControlConstraints = pageControlConstraints + self.initialPage = initialPage + self.dotSize = dotSize + super.init(nibName: nil, bundle: nil) + self.innerPages = setupInnerPages(for: views) + self.pageControl.currentPageIndicatorTintColor = indicatorColor + self.pageControl.backgroundColor = pageControlBackgroundColor + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadView() { + super.loadView() + + self.setupSubviews() + self.setupConstraints() + self.scrollView.delegate = self + } + + override public func viewDidLoad() { + super.viewDidLoad() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.didSwitchPages?(0) + + self.setupGestures() + self.viewDidLayoutSubviews() + } + + override public func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if !self.didInit { + self.didInit = true + self.scrollTo(page: self.initialPage, animated: false) + self.pageControl.currentPage = self.initialPage + } + + var lastFrame: CGRect = self.lastPageIndicator?.frame ?? .zero + lastFrame.origin = CGPoint(x: self.pageControl.frame.origin.x, y: self.pageControl.frame.origin.y) + + self.pageControl.subviews.enumerated().forEach { index, subview in + let currentFrame = subview.frame + + if let lastPageIndicator = self.lastPageIndicator, + (subview as? UIImageView != nil || index == self.pageControl.numberOfPages - 1) { + subview.removeFromSuperview() + let newFrame = CGRect( + x: lastFrame.origin.x + dotSize + 11, + y: lastFrame.origin.y - (lastPageIndicator.frame.height/2 - dotSize/2), + width: lastPageIndicator.frame.width, + height: lastPageIndicator.frame.height + ) + + self.lastPageIndicator?.frame = newFrame + self.lastPageIndicator?.image = lastPageIndicator.image?.tint( + with: self.pageControl.pageIndicatorTintColor! + ) + + pageControl.addSubview(self.lastPageIndicator!) + } else { + subview.frame = CGRect( + x: currentFrame.origin.x, + y: currentFrame.origin.y, + width: dotSize, + height: dotSize + ) + lastFrame = subview.frame + } + } + } + + /// Scrolls to the given page + /// + /// - Parameters: + /// - page: <#page description#> + /// - animated: <#animated description#> + public func scrollTo(page: Int, animated: Bool) { + self.scrollView.setContentOffset( + CGPoint(x: CGFloat(Int(scrollView.frame.size.width) * page), y: 0), + animated: animated + ) + pageControl.currentPage = page + self.viewDidLayoutSubviews() + } + + @objc func panned(sender: UIPanGestureRecognizer) { + let cgNumberOfPages: CGFloat = CGFloat(self.pageControl.numberOfPages) + var page: Int = Int(floor(Double((sender.location(in: self.view).x/pageControl.frame.width) * cgNumberOfPages))) + + page = (page >= pageControl.numberOfPages) + ? pageControl.numberOfPages - 1 + : (page < 0) + ? 0 + : page + + scrollTo(page: page, animated: false) + } +} + +// MARK: - View Setup +fileprivate extension SimplePagedView { + + fileprivate func setupInnerPages(for views: [UIView]) -> [UIView] { + if views.count == 0 { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .white + return [view] + } + + return views.map { $0.translatesAutoresizingMaskIntoConstraints = false; return $0 } + } + + fileprivate func setupGestures() { + if pageIndicatorIsInteractive { + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panned(sender:))) + + panGestureRecognizer.maximumNumberOfTouches = 1 + panGestureRecognizer.minimumNumberOfTouches = 1 + + pageControl.addGestureRecognizer(panGestureRecognizer) + } + } + + fileprivate func setupSubviews() { + let customView = ExternallyInteractiveUIView(frame: self.view.frame) + self.view = customView + + self.view.addSubview(scrollView) + self.scrollView.addSubview(scrollContentView) + + for page in innerPages { + scrollContentView.addSubview(page) + } + + pageControl.numberOfPages = innerPages.count + self.view.addSubview(pageControl) + } + + // swiftlint:disable next function_body_length + fileprivate func setupConstraints() { + let scrollViewConstraints = [ + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ] + + let innerViewConstraints = [ + scrollContentView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor), + scrollContentView.heightAnchor.constraint(equalTo: view.heightAnchor), + scrollContentView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: CGFloat(innerPages.count)), + scrollContentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + scrollContentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + scrollContentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + scrollContentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) + ] + + let pageConstraints: [NSLayoutConstraint] = { + var widthConstraints: [NSLayoutConstraint] = [] + var heightConstraints: [NSLayoutConstraint] = [] + var leadingEdgeConstraints: [NSLayoutConstraint] = [] + var topConstraints: [NSLayoutConstraint] = [] + var bottomConstraints: [NSLayoutConstraint] = [] + + for page in innerPages { + widthConstraints.append(page.widthAnchor.constraint(equalTo: view.widthAnchor)) + heightConstraints.append(page.heightAnchor.constraint(equalTo: view.heightAnchor)) + } + + leadingEdgeConstraints.append(innerPages[0].leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)) + + for index in 1..