├── Cartfile
├── Cartfile.resolved
├── AukTests
├── Images
│ ├── 35px.jpg
│ ├── 67px.png
│ └── 96px.png
├── TestHelpers
│ ├── iiFakeAnimatorParameter.swift
│ ├── iiFakeAnimator.swift
│ └── AukTestHelpers.swift
├── UIScrollView+AukTests.swift
├── Interface
│ ├── AukInterfaceNumberOfPagesTests.swift
│ ├── AukInterfaceRemoveAllTests.swift
│ ├── AukInterfaceImagesTests.swift
│ ├── AukInterfaceScrollToTests.swift
│ ├── AukInterfaceCurrentPageIndexTests.swift
│ ├── AukInterfaceUpdateLocalImageTests.swift
│ ├── AukInterfaceScrollNextPreviousPageTests.swift
│ ├── AukInterfaceShowLocalImageTests.swift
│ ├── AukInterfaceShowRemoteImageTests.swift
│ ├── AukInterfaceStartAutoScrollTests.swift
│ └── AukInterfaceUpdateRemoteImageTests.swift
├── Info.plist
├── AukScrollToTests.swift
├── AukScrollViewContentTests.swift
├── AukRemoteImageTests.swift
├── AukPageTests.swift
├── AukPageIndicatorContainerTests.swift
├── AukPageVisibilityTests.swift
├── AukPageVisibility_preloadImageTests.swift
└── AukTests.swift
├── Graphics
├── AppIcons
│ ├── 29.png
│ ├── 40.png
│ ├── 58.png
│ ├── 76.png
│ ├── 80.png
│ ├── 87.png
│ ├── 1024.png
│ ├── 120.png
│ ├── 152.png
│ ├── 167.png
│ └── 180.png
├── Drawings
│ ├── Pinguinus.jpg
│ ├── Wormius_Great_Auk.jpg
│ ├── Great_Auk_Egg_Bent.jpg
│ ├── Keulemans-GreatAuk.jpg
│ ├── Great_auk_with_juvenile.jpg
│ ├── Alca_Impennis_by_John_Gould.jpg
│ ├── John_James_Audubon_Great_Auk.jpg
│ └── popular_science_monthly_the_great_auk.jpg
├── Logo
│ ├── great_auk_logo.png
│ ├── great_auk_logo.sketch
│ └── great_auk_logo_small.sketch
├── ErrorImage
│ ├── error_image.png
│ └── error_image.pxm
├── Screenshots
│ ├── auk_demo_app.gif
│ ├── auk_demo_ios_app_2.jpg
│ ├── adjust_table_view_insets.png
│ ├── auk_paged_image_scroller_ios.jpg
│ └── auk_swift_slideshow_logging_console_2.png
└── placeholder
│ ├── great_auk_placeholder.png
│ └── great_auk_placeholder.pxm
├── Demo
├── Images
│ ├── error_image.png
│ ├── great_auk_logo.png
│ ├── Drawings
│ │ ├── Pinguinus.jpg
│ │ ├── Wormius_Great_Auk.jpg
│ │ ├── great_auk_placeholder.png
│ │ ├── John_James_Audubon_Great_Auk.jpg
│ │ └── popular_science_monthly_the_great_auk.jpg
│ └── great_auk_placeholder.png
├── Images.xcassets
│ └── AppIcon.appiconset
│ │ ├── 120.png
│ │ ├── 152.png
│ │ ├── 167.png
│ │ ├── 180.png
│ │ ├── 29.png
│ │ ├── 40.png
│ │ ├── 58.png
│ │ ├── 76.png
│ │ ├── 80.png
│ │ ├── 87.png
│ │ ├── 120-1.png
│ │ ├── 58-1.png
│ │ ├── 80-1.png
│ │ └── Contents.json
├── DemoConstants.swift
├── Info.plist
├── AukObjCBridge.swift
├── AppDelegate.swift
├── Base.lproj
│ └── LaunchScreen.xib
└── ViewController.swift
├── Auk.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── ConcatenateSwiftFiles.xcscheme
│ ├── TheAukTests.xcscheme
│ ├── Demo.xcscheme
│ └── GreatAuk.xcscheme
├── ISSUE_TEMPLATE.md
├── .gitignore
├── Auk
├── Utils
│ ├── RightToLeft.swift
│ ├── iiQ.swift
│ ├── iiAnimator.swift
│ ├── AutoCancellingTimer.swift
│ └── iiAutolayoutConstraints.swift
├── Auk.h
├── Info.plist
├── AukAutoscroll.swift
├── UIScrollView+Auk.swift
├── AukRemoteImage.swift
├── AukScrollViewDelegate.swift
├── AukScrollTo.swift
├── AukSettings.swift
├── AukScrollViewContent.swift
├── AukPageVisibility.swift
├── AukPage.swift
└── AukPageIndicatorContainer.swift
├── moa
├── moa.h
└── Info.plist
├── Package.swift
├── LICENSE
├── Auk.podspec
├── scripts
└── concatenate_swift_files.sh
└── CHANGELOG.md
/Cartfile:
--------------------------------------------------------------------------------
1 | github "evgenyneu/moa" ~> 12.0
--------------------------------------------------------------------------------
/Cartfile.resolved:
--------------------------------------------------------------------------------
1 | github "evgenyneu/moa" "4.0.0"
2 |
--------------------------------------------------------------------------------
/AukTests/Images/35px.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/AukTests/Images/35px.jpg
--------------------------------------------------------------------------------
/AukTests/Images/67px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/AukTests/Images/67px.png
--------------------------------------------------------------------------------
/AukTests/Images/96px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/AukTests/Images/96px.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/29.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/40.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/58.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/76.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/80.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/87.png
--------------------------------------------------------------------------------
/Demo/Images/error_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images/error_image.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/1024.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/120.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/152.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/167.png
--------------------------------------------------------------------------------
/Graphics/AppIcons/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/AppIcons/180.png
--------------------------------------------------------------------------------
/Demo/Images/great_auk_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images/great_auk_logo.png
--------------------------------------------------------------------------------
/Graphics/Drawings/Pinguinus.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Drawings/Pinguinus.jpg
--------------------------------------------------------------------------------
/Graphics/Logo/great_auk_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Logo/great_auk_logo.png
--------------------------------------------------------------------------------
/Demo/Images/Drawings/Pinguinus.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images/Drawings/Pinguinus.jpg
--------------------------------------------------------------------------------
/Demo/Images/great_auk_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images/great_auk_placeholder.png
--------------------------------------------------------------------------------
/Graphics/ErrorImage/error_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/ErrorImage/error_image.png
--------------------------------------------------------------------------------
/Graphics/ErrorImage/error_image.pxm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/ErrorImage/error_image.pxm
--------------------------------------------------------------------------------
/Graphics/Logo/great_auk_logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Logo/great_auk_logo.sketch
--------------------------------------------------------------------------------
/Graphics/Screenshots/auk_demo_app.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Screenshots/auk_demo_app.gif
--------------------------------------------------------------------------------
/Graphics/Drawings/Wormius_Great_Auk.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Drawings/Wormius_Great_Auk.jpg
--------------------------------------------------------------------------------
/Demo/Images/Drawings/Wormius_Great_Auk.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images/Drawings/Wormius_Great_Auk.jpg
--------------------------------------------------------------------------------
/Graphics/Drawings/Great_Auk_Egg_Bent.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Drawings/Great_Auk_Egg_Bent.jpg
--------------------------------------------------------------------------------
/Graphics/Drawings/Keulemans-GreatAuk.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Drawings/Keulemans-GreatAuk.jpg
--------------------------------------------------------------------------------
/Graphics/Logo/great_auk_logo_small.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Logo/great_auk_logo_small.sketch
--------------------------------------------------------------------------------
/Graphics/Screenshots/auk_demo_ios_app_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Screenshots/auk_demo_ios_app_2.jpg
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/Demo/Images/Drawings/great_auk_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images/Drawings/great_auk_placeholder.png
--------------------------------------------------------------------------------
/Graphics/Drawings/Great_auk_with_juvenile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Drawings/Great_auk_with_juvenile.jpg
--------------------------------------------------------------------------------
/Graphics/placeholder/great_auk_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/placeholder/great_auk_placeholder.png
--------------------------------------------------------------------------------
/Graphics/placeholder/great_auk_placeholder.pxm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/placeholder/great_auk_placeholder.pxm
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/120-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/120-1.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/58-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/58-1.png
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/80-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images.xcassets/AppIcon.appiconset/80-1.png
--------------------------------------------------------------------------------
/Graphics/Drawings/Alca_Impennis_by_John_Gould.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Drawings/Alca_Impennis_by_John_Gould.jpg
--------------------------------------------------------------------------------
/Graphics/Screenshots/adjust_table_view_insets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Screenshots/adjust_table_view_insets.png
--------------------------------------------------------------------------------
/Graphics/Drawings/John_James_Audubon_Great_Auk.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Drawings/John_James_Audubon_Great_Auk.jpg
--------------------------------------------------------------------------------
/Demo/Images/Drawings/John_James_Audubon_Great_Auk.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images/Drawings/John_James_Audubon_Great_Auk.jpg
--------------------------------------------------------------------------------
/Graphics/Screenshots/auk_paged_image_scroller_ios.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Screenshots/auk_paged_image_scroller_ios.jpg
--------------------------------------------------------------------------------
/Graphics/Drawings/popular_science_monthly_the_great_auk.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Drawings/popular_science_monthly_the_great_auk.jpg
--------------------------------------------------------------------------------
/Demo/Images/Drawings/popular_science_monthly_the_great_auk.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Demo/Images/Drawings/popular_science_monthly_the_great_auk.jpg
--------------------------------------------------------------------------------
/Graphics/Screenshots/auk_swift_slideshow_logging_console_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evgenyneu/Auk/HEAD/Graphics/Screenshots/auk_swift_slideshow_logging_console_2.png
--------------------------------------------------------------------------------
/Auk.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Please consider submitting the following information (if relevant):
2 |
3 | * Library setup method: file, Carthage, CocoaPods or Swift Package Manager.
4 | * Version of the library. Example: 8.0.
5 | * Xcode version. Example: 8.3.3.
6 | * OS version. Example: iOS 10.3.2.
--------------------------------------------------------------------------------
/Auk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | ./build
4 | *.pbxuser
5 | !default.pbxuser
6 | *.mode1v3
7 | !default.mode1v3
8 | *.mode2v3
9 | !default.mode2v3
10 | *.perspectivev3
11 | !default.perspectivev3
12 | xcuserdata
13 | *.xccheckout
14 | *.moved-aside
15 | DerivedData
16 | *.hmap
17 | *.ipa
18 | *.xcuserstate
19 | .DS_Store
20 | Carthage/Checkouts
21 | Gemfile
22 | Gemfile.lock
--------------------------------------------------------------------------------
/Auk/Utils/RightToLeft.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /**
4 |
5 | Helper functions for dealing with right-to-left languages.
6 |
7 | */
8 | struct RightToLeft {
9 | static func isRightToLeft(_ view: UIView) -> Bool {
10 | if #available(iOS 9.0, *) {
11 | return UIView.userInterfaceLayoutDirection(
12 | for: view.semanticContentAttribute) == .rightToLeft
13 | } else {
14 | return UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/AukTests/TestHelpers/iiFakeAnimatorParameter.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | // Parameters passed to the fake animator. Used in unit tests to verify animation
4 | struct iiFakeAnimatorParameter {
5 | // Arbitrary name of the animation
6 | var name: String
7 |
8 | // Animation duration
9 | var duration: TimeInterval
10 |
11 | // A function passed to the animator
12 | var animation: ()->()
13 |
14 | // A completion function passed to the animator
15 | var completion: ((Bool)->())?
16 | }
17 |
--------------------------------------------------------------------------------
/moa/moa.h:
--------------------------------------------------------------------------------
1 | //
2 | // moa.h
3 | // moa
4 | //
5 | // Created by Evgenii on 22/06/2016.
6 | // Copyright © 2016 Evgenii Neumerzhitckii. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for moa.
12 | FOUNDATION_EXPORT double moaVersionNumber;
13 |
14 | //! Project version string for moa.
15 | FOUNDATION_EXPORT const unsigned char moaVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/AukTests/UIScrollView+AukTests.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import XCTest
3 | @testable import Auk
4 |
5 | class UIScrollViewAukExtensionTests: XCTestCase {
6 | func testGetCreatesAndStoresMoaInstance() {
7 | let scrollView = UIScrollView()
8 | let auk1 = scrollView.auk
9 | let auk2 = scrollView.auk
10 |
11 | XCTAssert(auk1 === auk2)
12 | }
13 |
14 | func testSet() {
15 | let scrollView = UIScrollView()
16 | let auk = Auk(scrollView: scrollView)
17 | scrollView.auk = auk
18 |
19 | XCTAssert(scrollView.auk === auk)
20 | }
21 | }
--------------------------------------------------------------------------------
/Auk/Auk.h:
--------------------------------------------------------------------------------
1 | //
2 | // GreatAuk.h
3 | // GreatAuk
4 | //
5 | // Created by Evgenii on 13/06/2015.
6 | // Copyright (c) 2015 Evgenii Neumerzhitckii. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for GreatAuk.
12 | FOUNDATION_EXPORT double GreatAukVersionNumber;
13 |
14 | //! Project version string for GreatAuk.
15 | FOUNDATION_EXPORT const unsigned char GreatAukVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.2
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Auk",
6 | products: [
7 | .library(name: "Auk", targets: ["Auk"]),
8 | ],
9 | dependencies: [
10 | .package(
11 | url: "https://github.com/evgenyneu/moa.git",
12 | from: "12.0.0")
13 | ],
14 | targets: [
15 | .target(name: "Auk", dependencies: ["moa"], path: "Auk"),
16 | .testTarget(
17 | name: "AukTests",
18 | dependencies: ["Auk"],
19 | path: "AukTests"
20 | )
21 | ]
22 | )
23 |
--------------------------------------------------------------------------------
/AukTests/TestHelpers/iiFakeAnimator.swift:
--------------------------------------------------------------------------------
1 | @testable import Auk
2 |
3 | /// A helper class for running fake animations in unit tests
4 | class iiFakeAnimator: iiAnimator {
5 | // Array of animation functions
6 | var testParameters = [iiFakeAnimatorParameter]()
7 |
8 | /// A fake animation function that will be called instead of the real one in unit tests
9 | override func animate(name: String, withDuration duration: TimeInterval, animations: @escaping ()->(), completion: ((Bool)->())? = nil) {
10 |
11 | let parameter = iiFakeAnimatorParameter(name: name, duration: duration,
12 | animation: animations, completion: completion)
13 |
14 | testParameters.append(parameter)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceNumberOfPagesTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import UIKit
3 | @testable import Auk
4 |
5 | class AukInterfaceNumberOfPagesTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | var auk: Auk!
9 |
10 | override func setUp() {
11 | super.setUp()
12 |
13 | scrollView = UIScrollView()
14 |
15 | // Set scroll view size
16 | let size = CGSize(width: 120, height: 90)
17 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
18 |
19 | auk = Auk(scrollView: scrollView)
20 | }
21 |
22 | func testNumberOfPage() {
23 | // Show 2 images
24 | // -------------
25 |
26 | let image = createImage96px()
27 | auk.show(image: image)
28 | auk.show(image: image)
29 |
30 | XCTAssertEqual(2, auk.numberOfPages)
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/AukTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Auk/Utils/iiQ.swift:
--------------------------------------------------------------------------------
1 | //
2 | // iiQueue.swift
3 | //
4 | // Shortcut functions to run code in asynchronously and in main queue
5 | //
6 | // Created by Evgenii Neumerzhitckii on 11/10/2014.
7 | // Copyright (c) 2014 Evgenii Neumerzhitckii. All rights reserved.
8 | //
9 |
10 | import UIKit
11 |
12 | class iiQ {
13 | class func async(_ block: @escaping ()->()) {
14 | DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async(execute: block)
15 | }
16 |
17 | class func main(_ block: @escaping ()->()) {
18 | DispatchQueue.main.async(execute: block)
19 | }
20 |
21 | class func runAfterDelay(_ delaySeconds: Double, block: @escaping ()->()) {
22 | let time = DispatchTime.now() + Double(Int64(delaySeconds * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
23 | DispatchQueue.main.asyncAfter(deadline: time, execute: block)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Auk/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/moa/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2015 Evgenii Neumerzhitckii
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Auk/AukAutoscroll.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /**
4 |
5 | Starts and cancels the auto scrolling.
6 |
7 | */
8 | struct AukAutoscroll {
9 | var autoscrollTimer: AutoCancellingTimer?
10 |
11 | mutating func startAutoScroll(_ scrollView: UIScrollView, delaySeconds: Double,
12 | forward: Bool, cycle: Bool, animated: Bool, auk: Auk) {
13 |
14 | // Assign the new instance of AutoCancellingTimer to autoscrollTimer
15 | // The previous instance deinitializes and cancels its timer.
16 |
17 | autoscrollTimer = AutoCancellingTimer(interval: delaySeconds, repeats: true) {
18 | guard let currentPageIndex = auk.currentPageIndex else { return }
19 |
20 | if forward {
21 | AukScrollTo.scrollToNextPage(scrollView, cycle: cycle,
22 | animated: animated, currentPageIndex: currentPageIndex,
23 | numberOfPages: auk.numberOfPages)
24 | } else {
25 | AukScrollTo.scrollToPreviousPage(scrollView, cycle: cycle,
26 | animated: animated, currentPageIndex: currentPageIndex,
27 | numberOfPages: auk.numberOfPages)
28 | }
29 | }
30 | }
31 |
32 | mutating func stopAutoScroll() {
33 | autoscrollTimer = nil // Cancels the timer on deinit
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceRemoveAllTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import UIKit
3 | @testable import Auk
4 |
5 | class AukInterfaceRemoveAllTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | var auk: Auk!
9 |
10 | override func setUp() {
11 | super.setUp()
12 |
13 | scrollView = UIScrollView()
14 |
15 | // Set scroll view size
16 | let size = CGSize(width: 120, height: 90)
17 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
18 |
19 | auk = Auk(scrollView: scrollView)
20 | }
21 |
22 | func testRemoveImages() {
23 | // Layout scroll view
24 | // ---------------
25 |
26 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
27 | superview.addSubview(scrollView)
28 |
29 | let image = createImage96px()
30 | auk.show(image: image)
31 | auk.show(image: image)
32 | auk.show(image: image)
33 |
34 | auk.removeAll()
35 |
36 | XCTAssertEqual(0, aukPages(scrollView).count)
37 | XCTAssertEqual(0, auk.numberOfPages)
38 |
39 | XCTAssertEqual(0, auk.pageIndicatorContainer!.pageControl!.numberOfPages)
40 | XCTAssertEqual(0, auk.pageIndicatorContainer!.pageControl!.currentPage)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Auk.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "Auk"
3 | s.version = "11.0.0"
4 | s.license = { :type => "MIT" }
5 | s.homepage = "https://github.com/evgenyneu/Auk"
6 | s.summary = "An image slideshow for iOS written in Swift."
7 | s.description = <<-DESC
8 | This is an iOS library that shows an image carousel with a page indicator. Users can scroll through local and remote images or watch them scroll automatically.
9 |
10 | * Allows to specify placeholder and error images for remote sources.
11 | * Includes ability to simulate and verify image download in unit tests.
12 | * Supports animated transition during screen orientation change.
13 | * Includes image caching.
14 | * Supports right-to-left languages.
15 | DESC
16 | s.authors = { "Evgenii Neumerzhitckii" => "sausageskin@gmail.com" }
17 | s.source = { :git => "https://github.com/evgenyneu/Auk.git", :tag => s.version }
18 | s.screenshots = "https://raw.githubusercontent.com/evgenyneu/Auk/master/Graphics/Screenshots/auk_paged_image_scroller_ios.jpg"
19 | s.source_files = "Auk/**/*.swift"
20 | s.ios.deployment_target = "8.0"
21 | s.dependency "moa"
22 | s.swift_versions = ["4.2", "5.0"]
23 | end
--------------------------------------------------------------------------------
/Auk/UIScrollView+Auk.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | private var xoAukAssociationKey: UInt8 = 0
4 |
5 | /**
6 |
7 | Scroll view extension for showing series of images with page indicator.
8 |
9 |
10 | Usage:
11 |
12 | // Show remote image
13 | scrollView.auk.show(url: "http://site.com/bird.jpg")
14 |
15 | // Show local image
16 | if let image = UIImage(named: "bird.jpg") {
17 | scrollView.auk.show(image: image)
18 | }
19 |
20 | */
21 | public extension UIScrollView {
22 | /**
23 |
24 | Scroll view extension for showing series of images with page indicator.
25 |
26 | Usage:
27 |
28 | // Show remote image
29 | scrollView.auk.show(url: "http://site.com/bird.jpg")
30 |
31 | // Show local image
32 | if let image = UIImage(named: "bird.jpg") {
33 | scrollView.auk.show(image: image)
34 | }
35 |
36 | */
37 | var auk: Auk {
38 | get {
39 | if let value = objc_getAssociatedObject(self, &xoAukAssociationKey) as? Auk {
40 | return value
41 | } else {
42 | let auk = Auk(scrollView: self)
43 |
44 | objc_setAssociatedObject(self, &xoAukAssociationKey, auk,
45 | objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
46 |
47 | return auk
48 | }
49 | }
50 |
51 | set {
52 | objc_setAssociatedObject(self, &xoAukAssociationKey, newValue,
53 | objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceImagesTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import UIKit
3 | import moa
4 | @testable import Auk
5 |
6 | class AukInterfaceImagesTests: XCTestCase {
7 |
8 | var scrollView: UIScrollView!
9 | var auk: Auk!
10 |
11 | override func setUp() {
12 | super.setUp()
13 |
14 | scrollView = UIScrollView()
15 |
16 | // Set scroll view size
17 | let size = CGSize(width: 120, height: 90)
18 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
19 |
20 | auk = Auk(scrollView: scrollView)
21 | }
22 |
23 | override func tearDown() {
24 | super.tearDown()
25 |
26 | MoaSimulator.clear()
27 | }
28 |
29 | func testReturnImages() {
30 | let simulator = MoaSimulator.simulate("site.com")
31 | auk.show(url: "http://site.com/moa.png")
32 | simulator.respondWithImage(createImage67px())
33 |
34 | auk.show(image: createImage35px())
35 | auk.show(image: createImage96px())
36 |
37 | // Returns three images
38 | // -------------
39 |
40 | XCTAssertEqual(3, auk.images.count)
41 | XCTAssertEqual(67, auk.images[0].size.width)
42 | XCTAssertEqual(35, auk.images[1].size.width)
43 | XCTAssertEqual(96, auk.images[2].size.width)
44 | }
45 |
46 | func testDoesNotReturnPlaceholderImage() {
47 | auk.settings.placeholderImage = createImage35px()
48 | _ = MoaSimulator.simulate("site.com")
49 | auk.show(url: "http://site.com/moa.png")
50 |
51 | XCTAssertEqual(0, auk.images.count)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Auk/Utils/iiAnimator.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /**
4 |
5 | Collection of static function for animation.
6 |
7 | */
8 | class iiAnimator {
9 | // The object used for animation. This property is nil usually. In unit tests it contains a fake animator.
10 | static var currentAnimator: iiAnimator?
11 |
12 | // The object used for animation.
13 | static var animator: iiAnimator {
14 | get {
15 | return currentAnimator ?? iiAnimator()
16 | }
17 | }
18 |
19 | /// Animation function. This is a wrapper around UIView.animate to make it easier to unit test.
20 | func animate(name: String, withDuration duration: TimeInterval, animations: @escaping ()->(), completion: ((Bool)->())? = nil) {
21 | UIView.animate(withDuration: duration,
22 | animations: animations,
23 | completion: completion
24 | )
25 | }
26 |
27 |
28 |
29 | /**
30 |
31 | Fades out the view.
32 |
33 | - parameter view: View to fade out.
34 |
35 | - parameter animated: animates the fade out then true. Fades out immediately when false.
36 |
37 | - parameter withDuration: Duration of the fade out animation in seconds.
38 |
39 | - parameter completion: function to be called when the fade out animation is finished. Called immediately when not animated.
40 |
41 | */
42 | static func fadeOut(view: UIView, animated: Bool, withDuration duration: TimeInterval, completion: @escaping ()->()) {
43 | if animated {
44 | animator.animate(name: "Fade out", withDuration: duration,
45 | animations: {
46 | view.alpha = 0
47 | },
48 | completion: { _ in
49 | completion()
50 | }
51 | )
52 | } else {
53 | completion()
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Demo/DemoConstants.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | struct DemoConstants {
4 | static let button = DemoConstantsButton()
5 |
6 | static let initialImage = (
7 | fileName: "John_James_Audubon_Great_Auk.jpg",
8 | description: "The Great Auk drawing by John James Audubon, 1827-1838."
9 | )
10 |
11 | static let localImages = [
12 | (
13 | fileName: "Pinguinus.jpg",
14 | description: "The Great Auks at Home, oil on canvas by John Gerrard Keulemans."
15 | ),
16 | (
17 | fileName: "popular_science_monthly_the_great_auk.jpg",
18 | description: "The Great Auk drawing from Popular Science Monthly Volume 62, 1902-1903."
19 | )
20 | ]
21 |
22 | static let remoteImageBaseUrl = "http://evgenii.com/files/2015/06/auk_demo/"
23 |
24 | static let remoteImages = [
25 | (
26 | fileName: "Alca_Impennis_by_John_Gould.jpg",
27 | description: "Alca impennis by John Gould: The Birds of Europe, vol. 5 pl. 55, 19th century."
28 | ),
29 | (
30 | fileName: "Great_Auk_Egg_Bent.jpg",
31 | description: "Great Auk egg, U. S. National Museum, in a book by Arthur Cleveland Bent, 1919."
32 | ),
33 | (
34 | fileName: "Great_auk_with_juvenile.jpg",
35 | description: "Great auk with juvenile drawing by John Gerrard Keulemans, circa 1900."
36 | ),
37 | (
38 | fileName: "Keulemans-GreatAuk.jpg",
39 | description: "Great Auks in summer and winter plumage by John Gerrard Keulemans, before 1912."
40 | ),
41 | (
42 | fileName: "SimlulateNoImage.jpg",
43 | description: "Image download failure test."
44 | )
45 | ]
46 | }
47 |
48 | struct DemoConstantsButton {
49 | let borderWidth: CGFloat = 2
50 | let cornerRadius: CGFloat = 20
51 | let borderColor = UIColor.white
52 | }
53 |
--------------------------------------------------------------------------------
/Demo/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "29x29",
5 | "idiom" : "iphone",
6 | "filename" : "58-1.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "29x29",
11 | "idiom" : "iphone",
12 | "filename" : "87.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "40x40",
17 | "idiom" : "iphone",
18 | "filename" : "80-1.png",
19 | "scale" : "2x"
20 | },
21 | {
22 | "size" : "40x40",
23 | "idiom" : "iphone",
24 | "filename" : "120-1.png",
25 | "scale" : "3x"
26 | },
27 | {
28 | "size" : "60x60",
29 | "idiom" : "iphone",
30 | "filename" : "120.png",
31 | "scale" : "2x"
32 | },
33 | {
34 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "180.png",
37 | "scale" : "3x"
38 | },
39 | {
40 | "size" : "29x29",
41 | "idiom" : "ipad",
42 | "filename" : "29.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "29x29",
47 | "idiom" : "ipad",
48 | "filename" : "58.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "40x40",
53 | "idiom" : "ipad",
54 | "filename" : "40.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "40x40",
59 | "idiom" : "ipad",
60 | "filename" : "80.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "76x76",
65 | "idiom" : "ipad",
66 | "filename" : "76.png",
67 | "scale" : "1x"
68 | },
69 | {
70 | "size" : "76x76",
71 | "idiom" : "ipad",
72 | "filename" : "152.png",
73 | "scale" : "2x"
74 | },
75 | {
76 | "size" : "83.5x83.5",
77 | "idiom" : "ipad",
78 | "filename" : "167.png",
79 | "scale" : "2x"
80 | }
81 | ],
82 | "info" : {
83 | "version" : 1,
84 | "author" : "xcode"
85 | }
86 | }
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceScrollToTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import UIKit
3 | @testable import Auk
4 |
5 | class AukInterfaceScrollToTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | var auk: Auk!
9 |
10 | override func setUp() {
11 | super.setUp()
12 |
13 | scrollView = UIScrollView()
14 |
15 | // Set scroll view size
16 | let size = CGSize(width: 120, height: 90)
17 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
18 |
19 | auk = Auk(scrollView: scrollView)
20 | }
21 |
22 | // MARK: - Scroll to offset
23 |
24 | func testScrollTo() {
25 | let image = createImage96px()
26 | auk.show(image: image)
27 | auk.show(image: image)
28 | auk.show(image: image)
29 |
30 | auk.scrollToPage(atIndex: 2, animated: false)
31 | XCTAssertEqual(240, scrollView.contentOffset.x)
32 | }
33 |
34 | func testScrollTo_noPages() {
35 | auk.scrollToPage(atIndex: 0, animated: false)
36 | XCTAssertEqual(0, scrollView.contentOffset.x)
37 | }
38 |
39 | func testScrollTo_preventOverscrollingToTheRight() {
40 | let image = createImage96px()
41 | auk.show(image: image)
42 | auk.show(image: image)
43 | auk.show(image: image)
44 |
45 | auk.scrollToPage(atIndex: 3, animated: false)
46 | XCTAssertEqual(240, scrollView.contentOffset.x)
47 | }
48 |
49 | func testScrollTo_preventOverscrollingToTheLeft() {
50 | let image = createImage96px()
51 | auk.show(image: image)
52 | auk.show(image: image)
53 | auk.show(image: image)
54 |
55 | auk.scrollToPage(atIndex: -1, animated: false)
56 | XCTAssertEqual(0, scrollView.contentOffset.x)
57 | }
58 |
59 | // MARK: - Scroll to offset with width
60 |
61 | func testScrollToWithWidth() {
62 | let image = createImage96px()
63 | auk.show(image: image)
64 | auk.show(image: image)
65 | auk.show(image: image)
66 |
67 | auk.scrollToPage(atIndex: 2, pageWidth: 128, animated: false)
68 | XCTAssertEqual(256, scrollView.contentOffset.x)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Demo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Auk
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSExceptionDomains
30 |
31 | evgenii.com
32 |
33 | NSIncludesSubdomains
34 |
35 | NSTemporaryExceptionAllowsInsecureHTTPLoads
36 |
37 |
38 |
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIMainStoryboardFile
43 | Main
44 | UIRequiredDeviceCapabilities
45 |
46 | armv7
47 |
48 | UISupportedInterfaceOrientations
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 | UIInterfaceOrientationPortraitUpsideDown
54 |
55 | UISupportedInterfaceOrientations~ipad
56 |
57 | UIInterfaceOrientationPortrait
58 | UIInterfaceOrientationPortraitUpsideDown
59 | UIInterfaceOrientationLandscapeLeft
60 | UIInterfaceOrientationLandscapeRight
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/Auk/Utils/AutoCancellingTimer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Creates a timer that executes code after delay. The timer lives in an instance of `AutoCancellingTimer` class and is automatically canceled when this instance is deallocated.
3 | // This is an auto-canceling alternative to timer created with `dispatch_after` function.
4 | //
5 | // Source: https://gist.github.com/evgenyneu/516f7dcdb5f2f73d7923
6 | //
7 | // Usage
8 | // -----
9 | //
10 | // class MyClass {
11 | // var timer: AutoCancellingTimer? // Timer will be cancelled with MyCall is deallocated
12 | //
13 | // func runTimer() {
14 | // timer = AutoCancellingTimer(interval: delaySeconds, repeats: true) {
15 | // ... code to run
16 | // }
17 | // }
18 | // }
19 | //
20 | //
21 | // Cancel the timer
22 | // --------------------
23 | //
24 | // Timer is canceled automatically when it is deallocated. You can also cancel it manually:
25 | //
26 | // timer.cancel()
27 | //
28 |
29 | import UIKit
30 |
31 | final class AutoCancellingTimer {
32 | private var timer: AutoCancellingTimerInstance?
33 |
34 | init(interval: TimeInterval, repeats: Bool = false, callback: @escaping ()->()) {
35 | timer = AutoCancellingTimerInstance(interval: interval, repeats: repeats, callback: callback)
36 | }
37 |
38 | deinit {
39 | timer?.cancel()
40 | }
41 |
42 | func cancel() {
43 | timer?.cancel()
44 | }
45 | }
46 |
47 | final class AutoCancellingTimerInstance: NSObject {
48 | private let repeats: Bool
49 | private var timer: Timer?
50 | private var callback: ()->()
51 |
52 | init(interval: TimeInterval, repeats: Bool = false, callback: @escaping ()->()) {
53 | self.repeats = repeats
54 | self.callback = callback
55 |
56 | super.init()
57 |
58 | timer = Timer.scheduledTimer(timeInterval: interval, target: self,
59 | selector: #selector(AutoCancellingTimerInstance.timerFired(_:)), userInfo: nil, repeats: repeats)
60 | }
61 |
62 | func cancel() {
63 | timer?.invalidate()
64 | }
65 |
66 | @objc func timerFired(_ timer: Timer) {
67 | self.callback()
68 | if !repeats { cancel() }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Demo/AukObjCBridge.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /**
4 |
5 | This is a sample file that can be used in your ObjC project if you want to use Auk Swift library.
6 | Extend this file to add other functionality for your app.
7 |
8 | How to use
9 | ----------
10 |
11 | 1. Import swift code in your ObjC file:
12 |
13 | #import "YOUR_PRODUCT_MODULE_NAME-Swift.h"
14 |
15 | 2. Use Auk in your ObjC code:
16 |
17 | - (void)viewDidLoad {
18 | [super viewDidLoad];
19 |
20 | [AukObjCBridge setupWithScrollView: self.scrollView];
21 | [AukObjCBridge showWithUrl: @"https://bit.ly/auk_image" inScrollView: self.scrollView];
22 | [AukObjCBridge showWithUrl: @"https://bit.ly/moa_image" inScrollView: self.scrollView];
23 | }
24 | */
25 | @objc public class AukObjCBridge: NSObject {
26 | /**
27 |
28 | Downloads a remote image and adds it to the scroll view. Use `Moa.settings.cache` property to configure image caching.
29 |
30 | - parameter url: Url of the image to be shown.
31 |
32 | - parameter scrollView: A scroll view where the image will be shown.
33 |
34 |
35 | */
36 | public class func show(url: String, inScrollView scrollView: UIScrollView) {
37 | scrollView.auk.show(url: url)
38 | }
39 |
40 | /**
41 |
42 | Shows a local image in the scroll view.
43 |
44 | - parameter image: Image to be shown in the scroll view.
45 |
46 | - parameter scrollView: A scroll view with Auk images to remove.
47 |
48 | */
49 | public class func show(image: UIImage, inScrollView scrollView: UIScrollView) {
50 | scrollView.auk.show(image: image)
51 | }
52 |
53 | /**
54 |
55 | Removes all images from the scroll view.
56 |
57 | - parameter scrollView: A scroll view with Auk images to remove.
58 |
59 | */
60 | public class func removeAll(scrollView: UIScrollView) {
61 | scrollView.auk.removeAll()
62 | }
63 |
64 | /**
65 |
66 | Example of a function that changes Auk settings
67 |
68 | - parameter scrollView: a scroll view for chaging Auk settings
69 |
70 | */
71 | public class func setup(scrollView: UIScrollView) {
72 | scrollView.auk.settings.contentMode = UIView.ContentMode.scaleAspectFill
73 | scrollView.auk.settings.preloadRemoteImagesAround = 1
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Demo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Demo
4 | //
5 | // Created by Evgenii on 14/06/2015.
6 | // Copyright (c) 2015 Evgenii Neumerzhitckii. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Demo/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Auk/AukRemoteImage.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import moa
3 |
4 | /**
5 |
6 | Downloads and shows a single remote image.
7 |
8 | */
9 | class AukRemoteImage {
10 | var url: String?
11 | weak var imageView: UIImageView?
12 | weak var placeholderImageView: UIImageView?
13 |
14 | init() { }
15 |
16 | /// True when image has been successfully downloaded
17 | var didFinishDownload = false
18 |
19 | func setup(_ url: String, imageView: UIImageView, placeholderImageView: UIImageView?,
20 | settings: AukSettings) {
21 |
22 | self.url = url
23 | self.imageView = imageView
24 | self.placeholderImageView = placeholderImageView
25 | setPlaceholderImage(settings)
26 | }
27 |
28 | /// Sends image download HTTP request.
29 | func downloadImage(_ settings: AukSettings) {
30 | if imageView?.moa.url != nil { return } // Download has already started
31 | if didFinishDownload { return } // Image has already been downloaded
32 |
33 | imageView?.moa.errorImage = settings.errorImage
34 |
35 | imageView?.moa.onSuccessAsync = { [weak self] image in
36 | self?.didReceiveImageAsync(image, settings: settings)
37 | return image
38 | }
39 |
40 | imageView?.moa.url = url
41 | }
42 |
43 | /// Cancel current image download HTTP request.
44 | func cancelDownload() {
45 | // Cancel current download by setting url to nil
46 | imageView?.moa.url = nil
47 | }
48 |
49 | func didReceiveImageAsync(_ image: UIImage, settings: AukSettings) {
50 | didFinishDownload = true
51 |
52 | iiQ.main { [weak self] in
53 | guard let imageView = self?.imageView else { return }
54 | AukRemoteImage.animateImageView(imageView, show: true, settings: settings)
55 |
56 | if let placeholderImageView = self?.placeholderImageView {
57 | AukRemoteImage.animateImageView(placeholderImageView, show: false, settings: settings)
58 | }
59 | }
60 | }
61 |
62 | private static func animateImageView(_ imageView: UIImageView, show: Bool, settings: AukSettings) {
63 | imageView.alpha = show ? 0: 1
64 | let interval = TimeInterval(settings.remoteImageAnimationIntervalSeconds)
65 |
66 | UIView.animate(withDuration: interval, animations: {
67 | imageView.alpha = show ? 1: 0
68 | })
69 | }
70 |
71 | private func setPlaceholderImage(_ settings: AukSettings) {
72 | if let placeholderImage = settings.placeholderImage,
73 | let placeholderImageView = placeholderImageView {
74 |
75 | placeholderImageView.image = placeholderImage
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/scripts/concatenate_swift_files.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #
4 | # Combines *.swift files into a single file. Used in Xcode to build a single swift distributive file.
5 | #
6 | # Here is how to use it in Xcode:
7 | #
8 | # 1. Create an "External build system" target.
9 | # 2. Click "Info" tab in target settings.
10 | # 3. In "Build Tool" field specify the path to this script file, for example: $PROJECT_DIR/scripts/concatenate_swift_files.sh
11 | # 4. In "Arguments" field specify the arguments, for example $PROJECT_DIR/YourSubDir $PROJECT_DIR/Distrib/Distrib.swift "// Your header"
12 | # 5. Build the target and it will concatenate your swift files into a single swift file.
13 | #
14 | # You can see an example of using the script in this project: https://github.com/evgenyneu/moa
15 | #
16 | # Usage
17 | # ------
18 | #
19 | # ./combine_swift_files.sh source_dir destination_file [optional_header_text] [remove_line_text]
20 | #
21 | #
22 | # Example
23 | # --------
24 | #
25 | # Use in external build tool in Xcode.
26 | #
27 | # Build tool:
28 | #
29 | # $PROJECT_DIR/scripts/concatenate_swift_files.sh
30 | #
31 | # Arguments:
32 | #
33 | # $PROJECT_DIR/MyProject $PROJECT_DIR/Distrib/MyDistrib.swift "// My header" "remove this line"
34 | #
35 |
36 | # Handle paths with spaces (http://unix.stackexchange.com/a/9499)
37 | IFS=$'\n'
38 |
39 | destination=$2
40 | headermessage=$3
41 | remove_text=$4
42 |
43 | if [ "$#" -lt 2 ]
44 | then
45 | echo "\nUsage:\n"
46 | echo " ./combine_swift_files.sh source_dir destination_file [optional_header_text] [remove text]\n"
47 | exit 1
48 | fi
49 |
50 | # Create empty destination file
51 | echo > "$destination";
52 | text=""
53 | destination_filename=$(basename "$destination")
54 |
55 | for swift in `find $1 ! -name "$destination_filename" -name "*.swift"`;
56 | do
57 | filename=$(basename "$swift")
58 |
59 | text="$text\n// ----------------------------";
60 | text="$text\n//";
61 | text="$text\n// ${filename}";
62 | text="$text\n//";
63 | text="$text\n// ----------------------------\n\n";
64 |
65 | if [ -n "$remove_text" ]
66 | then
67 | filecontent="$(cat "${swift}"| sed "/${remove_text}/d";)"
68 | else
69 | filecontent="$(cat "${swift}";)"
70 | fi
71 |
72 | text="$text$filecontent\n\n";
73 |
74 | echo "Combining $swift";
75 | done;
76 |
77 | # Add header message
78 | if [ -n "$headermessage" ]
79 | then
80 | text="$headermessage\n\n$text"
81 | fi
82 |
83 | # Write to destination file
84 | echo -e "$text" > "$destination"
85 |
86 | echo -e "\nSwift files combined into $destination"
87 |
88 |
89 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Auk version history
2 |
3 |
4 | ## 11.0 (2019-10-27)
5 |
6 | * Updated `moa` image loader to support images with `application/octet-stream` MIME type in iOS 13 (see [iOS 13 Release Notes](https://developer.apple.com/documentation/ios_ipados_release_notes/ios_13_release_notes)).
7 |
8 |
9 | ## 10.0 (2019-04-20)
10 |
11 | * Update to Swift 5.0.
12 |
13 |
14 | ## 9.0 (2018-09-19)
15 |
16 | * Update to Swift 4.2.
17 |
18 |
19 | ## 8.0 (2017-09-23)
20 |
21 | * Update to Swift 4.0.
22 |
23 |
24 | ## 7.0 (2016-09-09)
25 |
26 | * Update to Xcode 8 GM version of Swift.
27 |
28 |
29 | ## 6.0 (2016-08-27)
30 |
31 | * Update to Xcode 8 Beta 6 version of Swift.
32 |
33 |
34 | ## 5.0 (2016-08-13)
35 |
36 | * Update to Xcode 8 Beta 5 version of Swift.
37 |
38 |
39 | ## 4.0 (2016-07-07)
40 |
41 | * Update to Xcode 8 Beta 2 version of Swift.
42 |
43 | * [Valpertui](https://github.com/Valpertui) added `removePage` and `removeCurrentPage` methods.
44 |
45 | * API change: method `scrollTo(2, animated: true)` was renamed to `scrollToPage(atIndex: 2, animated: true)`.
46 |
47 | * API change: method `updateAt(0, url: "https://bit.ly/auk_image")` was renamed to `updatePage(atIndex: 0, url: "https://bit.ly/auk_image")`.
48 |
49 | * API change: method `updateAt(1, image: image)` was renamed to `updatePage(atIndex: 1, image: image)`.
50 |
51 |
52 | ## 3.0 (2016-06-17)
53 |
54 | * Update to Swift 3.0
55 |
56 |
57 | ## 2.1.5 (2016-06-01)
58 |
59 | * Added support for loading remote images in GIF format and files with non-standard mime-type *image/jpg*.
60 |
61 |
62 | ## 2.1.4 (2016-04-29)
63 |
64 | * Added `settings.preloadRemoteImagesAround` property that controls the loading of remote images.
65 |
66 |
67 | ## 2.1.3 (2016-03-30)
68 |
69 | * Fixed the crash occured when the scroll view had zero bounds width.
70 |
71 |
72 | ## 2.1.2 (2016-03-27)
73 |
74 | * When updating an image with a remote image the current image is replaced only after the new image has finished downloading. This creates a smoother transition from the current image to the new image.
75 |
76 |
77 | ## 2.1.1 (2016-03-26)
78 |
79 | * Fixed: fade-in animation was not used when showing remote images without placeholders.
80 |
81 |
82 | ## 2.1.0 (2016-03-26)
83 |
84 | * [eyaldar](https://github.com/eyaldar) added `updateAt` method that allows to update an existing image with a new one.
85 | * Fixed a bug in `currentPageIndex` property that returned page indexes less than zero or greater than the largest page index.
86 | * Property `currentPageIndex` is optional and returns nil if there are no images.
87 | * Add new buttons to the demo app to test update of images.
88 |
89 |
90 | ## 2.0.19 (2015-11-13)
91 |
92 | * Fixed `images` property. Now it returns both local and remote images.
--------------------------------------------------------------------------------
/Auk/AukScrollViewDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /**
4 |
5 | This delegate detects the scrolling event which is used for loading remote images when their superview becomes visible on screen.
6 |
7 | */
8 | final class AukScrollViewDelegate: NSObject, UIScrollViewDelegate {
9 | /**
10 |
11 | If scroll view already has delegate it is preserved in this property and all the delegate calls are forwarded to it.
12 |
13 | */
14 | weak var delegate: UIScrollViewDelegate?
15 |
16 | var onScroll: (()->())?
17 | var onScrollByUser: (()->())?
18 |
19 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
20 | onScroll?()
21 | delegate?.scrollViewDidScroll?(scrollView)
22 | }
23 |
24 | func scrollViewDidZoom(_ scrollView: UIScrollView) {
25 | delegate?.scrollViewDidZoom?(scrollView)
26 | }
27 |
28 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
29 | delegate?.scrollViewWillBeginDragging?(scrollView)
30 | onScrollByUser?()
31 | }
32 |
33 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
34 |
35 | delegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
36 | }
37 |
38 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
39 | delegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
40 | }
41 |
42 | func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
43 | delegate?.scrollViewWillBeginDecelerating?(scrollView)
44 | }
45 |
46 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
47 | delegate?.scrollViewDidEndDecelerating?(scrollView)
48 | }
49 |
50 | func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
51 | delegate?.scrollViewDidEndScrollingAnimation?(scrollView)
52 | }
53 |
54 | func viewForZooming(in scrollView: UIScrollView) -> UIView? {
55 | return delegate?.viewForZooming?(in: scrollView)
56 | }
57 |
58 | func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
59 | delegate?.scrollViewWillBeginZooming?(scrollView, with: view)
60 | }
61 |
62 | func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
63 | delegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
64 | }
65 |
66 | func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
67 | return delegate?.scrollViewShouldScrollToTop?(scrollView) ?? true
68 | }
69 |
70 | func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
71 | delegate?.scrollViewDidScrollToTop?(scrollView)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Auk/AukScrollTo.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /**
4 |
5 | Scrolling code.
6 |
7 | */
8 | struct AukScrollTo {
9 | static func scrollToPage(_ scrollView: UIScrollView, atIndex index: Int, animated: Bool,
10 | numberOfPages: Int) {
11 |
12 | let pageWidth = scrollView.bounds.size.width
13 | scrollToPage(scrollView, atIndex: index, pageWidth: pageWidth, animated: animated,
14 | numberOfPages: numberOfPages)
15 | }
16 |
17 | static func scrollToPage(_ scrollView: UIScrollView, atIndex index: Int, pageWidth: CGFloat,
18 | animated: Bool, numberOfPages: Int) {
19 |
20 | let offsetX = contentOffsetForPage(atIndex: index, pageWidth: pageWidth,
21 | numberOfPages: numberOfPages, scrollView: scrollView)
22 |
23 | let offset = CGPoint(x: offsetX, y: 0)
24 |
25 | scrollView.setContentOffset(offset, animated: animated)
26 | }
27 |
28 | static func scrollToNextPage(_ scrollView: UIScrollView, cycle: Bool, animated: Bool,
29 | currentPageIndex: Int, numberOfPages: Int) {
30 |
31 | var pageIndex = currentPageIndex + 1
32 |
33 | if pageIndex >= numberOfPages {
34 | if cycle {
35 | pageIndex = 0
36 | } else {
37 | return
38 | }
39 | }
40 |
41 | scrollToPage(scrollView, atIndex: pageIndex, animated: animated, numberOfPages: numberOfPages)
42 | }
43 |
44 | static func scrollToPreviousPage(_ scrollView: UIScrollView, cycle: Bool, animated: Bool,
45 | currentPageIndex: Int, numberOfPages: Int) {
46 |
47 | var pageIndex = currentPageIndex - 1
48 |
49 | if pageIndex < 0 {
50 | if cycle {
51 | pageIndex = numberOfPages - 1
52 | } else {
53 | return
54 | }
55 | }
56 |
57 | scrollToPage(scrollView, atIndex: pageIndex, animated: animated, numberOfPages: numberOfPages)
58 | }
59 |
60 | /**
61 |
62 | Returns horizontal content offset needed to display the given page.
63 | Ensures that offset is within the content size.
64 |
65 | */
66 | static func contentOffsetForPage(atIndex index: Int, pageWidth: CGFloat,
67 | numberOfPages: Int, scrollView: UIView) -> CGFloat {
68 |
69 | // The index of the page that appears from left to right of the screen.
70 | // It is the same as pageIndex for left-to-right languages.
71 | let pageIndexFromTheLeft = RightToLeft.isRightToLeft(scrollView) ?
72 | numberOfPages - index - 1 : index
73 |
74 | var offsetX = CGFloat(pageIndexFromTheLeft) * pageWidth
75 |
76 | let maxOffset = CGFloat(numberOfPages - 1) * pageWidth
77 |
78 | // Prevent overscrolling to the right
79 | if offsetX > maxOffset { offsetX = maxOffset }
80 |
81 | // Prevent overscrolling to the left
82 | if offsetX < 0 { offsetX = 0 }
83 |
84 | return offsetX
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Auk/AukSettings.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /**
4 |
5 | Appearance and behavior of the scroll view.
6 |
7 | */
8 | public struct AukSettings {
9 |
10 | /// Determines the stretching and scaling of the image when its proportion are not the same as its container.
11 | public var contentMode = UIView.ContentMode.scaleAspectFit
12 |
13 | /// Image to be displayed when remote image download fails.
14 | public var errorImage: UIImage?
15 |
16 | /// Settings for styling the scroll view page indicator.
17 | public var pageControl = PageControlSettings()
18 |
19 | /// Enable paging for the scroll view. When true the view automatically scrolls to show the whole image.
20 | public var pagingEnabled = true
21 |
22 | /// Image to be displayed while the remote image is being downloaded.
23 | public var placeholderImage: UIImage?
24 |
25 | /**
26 |
27 | The number of remote images to preload around the current page. For example, if preloadRemoteImagesAround = 2 and we are viewing the first page it will preload images on the second and third pages. If we are viewing 5th page then it will preload images on pages 3, 4, 6 and 7 (unless they are already loaded). The default value is 0, i.e. it only loads the image for the currently visible pages.
28 |
29 | */
30 | public var preloadRemoteImagesAround = 0
31 |
32 | /// The duration of the animation that is used to show the remote images.
33 | public var remoteImageAnimationIntervalSeconds: Double = 0.5
34 |
35 | // Duration of the fade out animation when the page is removed.
36 | public var removePageFadeOutAnimationDurationSeconds: Double = 0.2
37 |
38 | // Duration of the layout animation when the page is removed.
39 | public var removePageLayoutAnimationDurationSeconds: Double = 0.3
40 |
41 | /// Show horizontal scroll indicator.
42 | public var showsHorizontalScrollIndicator = false
43 | }
44 |
45 | /**
46 |
47 | Settings for page indicator.
48 |
49 | */
50 | public struct PageControlSettings {
51 | /// Background color of the page control container view.
52 | public var backgroundColor = UIColor(red: 128/256, green: 128/256, blue: 128/256, alpha: 0.4)
53 |
54 | /// Corner radius of page control container view.
55 | public var cornerRadius: Double = 13
56 |
57 | /// Color of the dot representing for the current page.
58 | public var currentPageIndicatorTintColor: UIColor? = nil
59 |
60 | /// Padding between page indicator and its container
61 | public var innerPadding = CGSize(width: 10, height: -5)
62 |
63 | /// Distance between the bottom of the page control view and the bottom of the scroll view.
64 | public var marginToScrollViewBottom: Double = 8
65 |
66 | /// Color of the page indicator dot.
67 | public var pageIndicatorTintColor: UIColor? = nil
68 |
69 | /// When true the page control is visible on screen.
70 | public var visible = true
71 | }
72 |
--------------------------------------------------------------------------------
/Auk.xcodeproj/xcshareddata/xcschemes/ConcatenateSwiftFiles.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/Auk/AukScrollViewContent.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /**
4 |
5 | Collection of static functions that help managing the scroll view content.
6 |
7 | */
8 | struct AukScrollViewContent {
9 |
10 | /**
11 |
12 | - returns: Array of scroll view pages.
13 |
14 | */
15 | static func aukPages(_ scrollView: UIScrollView) -> [AukPage] {
16 | return scrollView.subviews.filter { $0 is AukPage }.map { $0 as! AukPage }
17 | }
18 |
19 | /**
20 |
21 | - returns: Page at index. Returns nil if index is out of bounds.
22 |
23 | */
24 | static func page(atIndex index: Int, scrollView: UIScrollView) -> AukPage? {
25 | let pages = aukPages(scrollView)
26 | if index < 0 { return nil }
27 | if index >= pages.count { return nil }
28 | return pages[index]
29 | }
30 |
31 | /**
32 |
33 | Creates Auto Layout constraints for positioning the page view inside the scroll view.
34 |
35 | - parameter scrollView: scroll view to layout.
36 |
37 | - parameter animated: will animate the layout if true. Default value: false.
38 |
39 | - parameter animationDurationInSeconds: duration of the layout animation. Ignored if `animated` parameter is false.
40 |
41 | - parameter completion: function that is called when layout animation finishes. Called immediately if not animated.
42 |
43 | */
44 | static func layout(_ scrollView: UIScrollView, animated: Bool = false,
45 | animationDurationInSeconds: Double = 0.2, completion: (()->())? = nil) {
46 |
47 | let pages = aukPages(scrollView)
48 |
49 | for (index, page) in pages.enumerated() {
50 |
51 | // Delete current constraints by removing the view and adding it back to its superview
52 | page.removeFromSuperview()
53 | scrollView.addSubview(page)
54 |
55 | page.translatesAutoresizingMaskIntoConstraints = false
56 |
57 | // Make page size equal to the scroll view size
58 | iiAutolayoutConstraints.equalSize(page, viewTwo: scrollView, constraintContainer: scrollView)
59 |
60 | // Stretch the page vertically to fill the height of the scroll view
61 | iiAutolayoutConstraints.fillParent(page, parentView: scrollView, margin: 0, vertically: true)
62 |
63 | if index == 0 {
64 | // Align the leading edge of the first page to the leading edge of the scroll view.
65 | iiAutolayoutConstraints.alignSameAttributes(page, toItem: scrollView,
66 | constraintContainer: scrollView, attribute: NSLayoutConstraint.Attribute.leading, margin: 0)
67 | }
68 |
69 | if index == pages.count - 1 {
70 | // Align the trailing edge of the last page to the trailing edge of the scroll view.
71 | iiAutolayoutConstraints.alignSameAttributes(page, toItem: scrollView,
72 | constraintContainer: scrollView, attribute: NSLayoutConstraint.Attribute.trailing, margin: 0)
73 | }
74 | }
75 |
76 | // Align page next to each other
77 | iiAutolayoutConstraints.viewsNextToEachOther(pages, constraintContainer: scrollView,
78 | margin: 0, vertically: false)
79 |
80 | if animated {
81 | iiAnimator.animator.animate(name: "layoutIfNeeded", withDuration: animationDurationInSeconds,
82 | animations: {
83 | scrollView.layoutIfNeeded()
84 | },
85 | completion: { _ in
86 | completion?()
87 | }
88 | )
89 | } else {
90 | scrollView.layoutIfNeeded()
91 | completion?()
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceCurrentPageIndexTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import UIKit
3 | @testable import Auk
4 |
5 | class AukInterfaceCurrentPageIndexTestsTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | var auk: Auk!
9 |
10 | override func setUp() {
11 | super.setUp()
12 |
13 | scrollView = UIScrollView()
14 |
15 | // Set scroll view size
16 | let size = CGSize(width: 120, height: 90)
17 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
18 |
19 | auk = Auk(scrollView: scrollView)
20 | }
21 |
22 | func testCurrentPageIndex() {
23 | // Show 2 images
24 | // -------------
25 |
26 | let image = createImage96px()
27 | auk.show(image: image)
28 | auk.show(image: image)
29 | auk.show(image: image)
30 |
31 | XCTAssertEqual(0, auk.currentPageIndex)
32 |
33 | // Scroll to show more than the half of the second page
34 | scrollView.contentOffset.x = 70
35 | XCTAssertEqual(1, auk.currentPageIndex)
36 |
37 | // Scroll to the second image
38 | scrollView.contentOffset.x = 120
39 | XCTAssertEqual(1, auk.currentPageIndex)
40 |
41 | // Scroll to show the third page ALMOST entirely
42 | scrollView.contentOffset.x = 230
43 | XCTAssertEqual(2, auk.currentPageIndex)
44 | }
45 |
46 | func testCurrentPageIndex_indexOutOfBounds() {
47 | let image = createImage96px()
48 | auk.show(image: image)
49 | auk.show(image: image)
50 |
51 | // Scrolled to the right over the rightmost image
52 |
53 | scrollView.contentOffset.x = 180
54 | XCTAssertEqual(1, auk.currentPageIndex)
55 |
56 | scrollView.contentOffset.x = 1800
57 | XCTAssertEqual(1, auk.currentPageIndex)
58 |
59 | // Scrolled to the left over the leftmost image
60 | scrollView.contentOffset.x = -70
61 | XCTAssertEqual(0, auk.currentPageIndex)
62 | }
63 |
64 | func testCurrentPageIndex_noImages() {
65 | XCTAssertNil(auk.currentPageIndex)
66 | }
67 |
68 | func testCurrenPageIndex_rightToLeft() {
69 | if #available(iOS 9.0, *) {
70 | scrollView.semanticContentAttribute = .forceRightToLeft
71 |
72 | // Show 2 images
73 | // -------------
74 |
75 | let image = createImage96px()
76 | auk.show(image: image)
77 | auk.show(image: image)
78 | auk.show(image: image)
79 |
80 | // Show first page by default
81 | XCTAssertEqual(0, auk.currentPageIndex)
82 |
83 | // Scroll to third page explicitely
84 | scrollView.contentOffset.x = 240
85 | XCTAssertEqual(0, auk.currentPageIndex)
86 |
87 | // Scroll to show more than the half of the second page
88 | scrollView.contentOffset.x = 140
89 | XCTAssertEqual(1, auk.currentPageIndex)
90 |
91 | // Scroll to the second image
92 | scrollView.contentOffset.x = 120
93 | XCTAssertEqual(1, auk.currentPageIndex)
94 |
95 | // Scroll to show the third page ALMOST entirely
96 | scrollView.contentOffset.x = 20
97 | XCTAssertEqual(2, auk.currentPageIndex)
98 | }
99 | }
100 |
101 | func testCurrentPageIndex_handleZeroWidth() {
102 | scrollView.bounds = CGRect(origin: CGPoint(), size: CGSize())
103 |
104 | let image = createImage96px()
105 | auk.show(image: image)
106 |
107 | XCTAssertNil(auk.currentPageIndex)
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Auk/AukPageVisibility.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /**
4 |
5 | Helper functions that tell if the scroll view page is currently visible to the user.
6 |
7 | */
8 | struct AukPageVisibility {
9 | /**
10 |
11 | Check if the given page is currently visible to user.
12 |
13 | - parameter scrollView: Scroll view containing the page.
14 | - parameter page: A scroll view page which visibility will be checked.
15 |
16 | - returns: True if the page is visible to the user.
17 |
18 | */
19 | static func isVisible(_ scrollView: UIScrollView, page: AukPage) -> Bool {
20 | return scrollView.bounds.intersects(page.frame)
21 | }
22 |
23 | /**
24 |
25 | Tells if the page is way out of sight. This is done to prevent cancelling download of the image for the page that is not very far out of sight.
26 |
27 | - parameter scrollView: Scroll view containing the page.
28 | - parameter page: A scroll view page which visibility will be checked.
29 |
30 | - returns: True if the page is visible to the user.
31 |
32 | */
33 | static func isFarOutOfSight(_ scrollView: UIScrollView, page: AukPage) -> Bool {
34 | let parentRectWithIncreasedHorizontalBounds = scrollView.bounds.insetBy(dx: -50, dy: 0)
35 | return !parentRectWithIncreasedHorizontalBounds.intersects(page.frame)
36 | }
37 |
38 | /**
39 |
40 | Go through all the scroll view pages and tell them if they are visible or out of sight.
41 | The pages, in turn, if they are visible start the download of the image
42 | or cancel the download if they are out of sight.
43 |
44 | - parameter scrollView: Scroll view with the pages.
45 |
46 | */
47 | static func tellPagesAboutTheirVisibility(_ scrollView: UIScrollView,
48 | settings: AukSettings,
49 | currentPageIndex: Int) {
50 |
51 | let pages = AukScrollViewContent.aukPages(scrollView)
52 |
53 | for (index, page) in pages.enumerated() {
54 | if isVisible(scrollView, page: page) {
55 | page.visibleNow(settings)
56 | } else {
57 | if abs(index - currentPageIndex) <= settings.preloadRemoteImagesAround {
58 | // Preload images for the pages around the current page
59 | page.visibleNow(settings)
60 | } else {
61 | /*
62 | The image is not visible to user and is not preloaded - cancel its download.
63 |
64 | Now, this is a bit nuanced so let me explain. When we scroll into a new page we sometimes see a little bit of the next page. The scroll view animation overshoots a little bit to show the next page and then slides back to the current page. This is probably done on purpose for more natural spring bouncing effect.
65 |
66 | When the scroll view overshoots and shows the next page, we call `isVisible` on it and it starts downloading its image. But because scroll view bounces back in a moment the page becomes invisible again very soon. If we just call `outOfSightNow()` the next page download will be canceled even though it has just been started. That is probably not very efficient use of network, so we call `isFarOutOfSight` function to check if the next page is way out of sight (and not just a little bit). If the page is out of sight but just by a little margin we still let it download the image.
67 |
68 | */
69 | if isFarOutOfSight(scrollView, page: page) {
70 | page.outOfSightNow()
71 | }
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceUpdateLocalImageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import UIKit
3 | @testable import Auk
4 |
5 | class AukInterfaceUpdateLocalImageTests: XCTestCase {
6 | var scrollView: UIScrollView!
7 | var auk: Auk!
8 |
9 | override func setUp() {
10 | super.setUp()
11 |
12 | scrollView = UIScrollView()
13 |
14 | // Set scroll view size
15 | let size = CGSize(width: 120, height: 90)
16 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
17 |
18 | auk = Auk(scrollView: scrollView)
19 | }
20 |
21 | func testUpdateLocalImage() {
22 | let image = createImage96px()
23 | auk.show(image: image)
24 |
25 | let image67px = createImage67px()
26 | auk.updatePage(atIndex: 0, image: image67px)
27 |
28 | XCTAssertEqual(1, aukPages(scrollView).count)
29 | XCTAssertEqual(67, firstAukImageWidth(scrollView, pageIndex: 0))
30 | }
31 |
32 | func testUpdateLocalImage_updateOnlyGivenSingePage() {
33 | // Show two images
34 | let image96px = createImage96px()
35 | auk.show(image: image96px)
36 |
37 | let image35px = createImage35px()
38 | auk.show(image: image35px)
39 |
40 | // Update image on the second page
41 | let image67px = createImage67px()
42 | auk.updatePage(atIndex: 1, image: image67px)
43 |
44 | XCTAssertEqual(2, aukPages(scrollView).count)
45 |
46 | // First page images remains unchanged
47 | XCTAssertEqual(96, firstAukImageWidth(scrollView, pageIndex: 0))
48 |
49 | // Second page image is updated
50 | XCTAssertEqual(67, firstAukImageWidth(scrollView, pageIndex: 1))
51 | }
52 |
53 | func testUpdateLocalImage_indexLargerThanExist() {
54 | let image = createImage96px()
55 | auk.show(image: image)
56 |
57 | let image67px = createImage67px()
58 | auk.updatePage(atIndex: 1, image: image67px)
59 |
60 | XCTAssertEqual(1, aukPages(scrollView).count)
61 | XCTAssertEqual(96, firstAukImageWidth(scrollView, pageIndex: 0))
62 | }
63 |
64 | func testUpdateLocalImage_indexNegative() {
65 | let image = createImage96px()
66 | auk.show(image: image)
67 |
68 | let image67px = createImage67px()
69 | auk.updatePage(atIndex: -1, image: image67px)
70 |
71 | XCTAssertEqual(1, aukPages(scrollView).count)
72 | XCTAssertEqual(96, firstAukImageWidth(scrollView, pageIndex: 0))
73 | }
74 |
75 | func testUpdateLocalImage_noImages() {
76 | let image67px = createImage67px()
77 | auk.updatePage(atIndex: 0, image: image67px)
78 | XCTAssertEqual(0, aukPages(scrollView).count)
79 | }
80 |
81 | // MARK: - Accessibility
82 |
83 | func testUpdateAccessiblePageView_withLabel() {
84 | let image = createImage96px()
85 | auk.show(image: image, accessibilityLabel: "Penguin")
86 |
87 | let image67px = createImage67px()
88 | auk.updatePage(atIndex: 0, image: image67px, accessibilityLabel: "White knight riding a wooden horse on wheels.")
89 |
90 | let page = aukPage(scrollView, pageIndex: 0)!
91 |
92 | XCTAssert(page.isAccessibilityElement)
93 | XCTAssertEqual(page.accessibilityTraits, UIAccessibilityTraits.image)
94 | XCTAssertEqual("White knight riding a wooden horse on wheels.", page.accessibilityLabel!)
95 | }
96 |
97 | func testUpdateAccessiblePageView_removeExistingLabel() {
98 | let image = createImage96px()
99 | auk.show(image: image, accessibilityLabel: "Penguin")
100 |
101 | let image67px = createImage67px()
102 | auk.updatePage(atIndex: 0, image: image67px)
103 |
104 | let page = aukPage(scrollView, pageIndex: 0)!
105 |
106 | XCTAssert(page.isAccessibilityElement)
107 | XCTAssertEqual(page.accessibilityTraits, UIAccessibilityTraits.image)
108 | XCTAssert(page.accessibilityLabel == nil)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceScrollNextPreviousPageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import UIKit
3 | @testable import Auk
4 |
5 | class AukInterfaceScrollNextPreviousPageTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | var auk: Auk!
9 |
10 | override func setUp() {
11 | super.setUp()
12 |
13 | scrollView = UIScrollView()
14 |
15 | // Set scroll view size
16 | let size = CGSize(width: 120, height: 90)
17 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
18 |
19 | auk = Auk(scrollView: scrollView)
20 | }
21 |
22 | // MARK: - Scroll to next page
23 |
24 | func testScrollToNextPage() {
25 | let image = createImage96px()
26 | auk.show(image: image)
27 | auk.show(image: image)
28 | auk.show(image: image)
29 |
30 | auk.scrollToNextPage()
31 | XCTAssertEqual(1, auk.currentPageIndex)
32 |
33 | auk.scrollToNextPage()
34 | XCTAssertEqual(2, auk.currentPageIndex)
35 |
36 | auk.scrollToNextPage()
37 | XCTAssertEqual(0, auk.currentPageIndex)
38 | }
39 |
40 | func testScrollToNextPage_withParameters_cycle() {
41 | let image = createImage96px()
42 | auk.show(image: image)
43 | auk.show(image: image)
44 | auk.show(image: image)
45 |
46 | auk.scrollToNextPage(cycle: true, animated: true)
47 | XCTAssertEqual(1, auk.currentPageIndex)
48 |
49 | auk.scrollToNextPage(cycle: true, animated: true)
50 | XCTAssertEqual(2, auk.currentPageIndex)
51 |
52 | auk.scrollToNextPage(cycle: true, animated: true)
53 | XCTAssertEqual(0, auk.currentPageIndex)
54 | }
55 |
56 | func testScrollToNextPage_withParameters_noCycle() {
57 | let image = createImage96px()
58 | auk.show(image: image)
59 | auk.show(image: image)
60 | auk.show(image: image)
61 |
62 | auk.scrollToNextPage(cycle: false, animated: true)
63 | XCTAssertEqual(1, auk.currentPageIndex)
64 |
65 | auk.scrollToNextPage(cycle: false, animated: true)
66 | XCTAssertEqual(2, auk.currentPageIndex)
67 |
68 | auk.scrollToNextPage(cycle: false, animated: true)
69 | XCTAssertEqual(2, auk.currentPageIndex)
70 | }
71 |
72 | // MARK: - Scroll to previous page
73 |
74 | func testScrollToPreviousPage() {
75 | let image = createImage96px()
76 | auk.show(image: image)
77 | auk.show(image: image)
78 | auk.show(image: image)
79 |
80 | auk.scrollToPreviousPage()
81 | XCTAssertEqual(2, auk.currentPageIndex)
82 |
83 | auk.scrollToPreviousPage()
84 | XCTAssertEqual(1, auk.currentPageIndex)
85 |
86 | auk.scrollToPreviousPage()
87 | XCTAssertEqual(0, auk.currentPageIndex)
88 | }
89 |
90 | func testScrollToPreviousPage_withParameters_cycle() {
91 | let image = createImage96px()
92 | auk.show(image: image)
93 | auk.show(image: image)
94 | auk.show(image: image)
95 |
96 | auk.scrollToPreviousPage(cycle: true, animated: true)
97 | XCTAssertEqual(2, auk.currentPageIndex)
98 |
99 | auk.scrollToPreviousPage(cycle: true, animated: true)
100 | XCTAssertEqual(1, auk.currentPageIndex)
101 |
102 | auk.scrollToPreviousPage(cycle: true, animated: true)
103 | XCTAssertEqual(0, auk.currentPageIndex)
104 | }
105 |
106 | func testScrollToPreviousPage_withParameters_noCycle() {
107 | let image = createImage96px()
108 | auk.show(image: image)
109 | auk.show(image: image)
110 | auk.show(image: image)
111 |
112 | auk.scrollToPage(atIndex: 2, animated: false)
113 |
114 | auk.scrollToPreviousPage(cycle: false, animated: true)
115 | XCTAssertEqual(1, auk.currentPageIndex)
116 |
117 | auk.scrollToPreviousPage(cycle: false, animated: true)
118 | XCTAssertEqual(0, auk.currentPageIndex)
119 |
120 | auk.scrollToPreviousPage(cycle: false, animated: true)
121 | XCTAssertEqual(0, auk.currentPageIndex)
122 | }
123 |
124 | }
125 |
--------------------------------------------------------------------------------
/AukTests/TestHelpers/AukTestHelpers.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import XCTest
3 | @testable import Auk
4 |
5 | /// Test helpers
6 | extension XCTestCase {
7 | func nsDataFromFile(_ name: String) -> Data {
8 | let url = Bundle(for: type(of: self)).url(forResource: name, withExtension: nil)
9 | return (try! Data(contentsOf: url!))
10 | }
11 |
12 | func createImage35px() -> UIImage {
13 | return uiImageFromFile("35px.jpg")
14 | }
15 |
16 | func createImage67px() -> UIImage {
17 | return uiImageFromFile("67px.png")
18 | }
19 |
20 | func createImage96px() -> UIImage {
21 | return uiImageFromFile("96px.png")
22 | }
23 |
24 | private func uiImageFromFile(_ name: String) -> UIImage {
25 | return UIImage(data: nsDataFromFile(name))!
26 | }
27 |
28 | /**
29 |
30 | - returns: Array of scroll view pages.
31 |
32 | */
33 | func aukPages(_ scrollView: UIScrollView) -> [AukPage] {
34 | return AukScrollViewContent.aukPages(scrollView)
35 | }
36 |
37 | /**
38 |
39 | - returns: The the AukPage with given index.
40 |
41 | */
42 | func aukPage(_ scrollView: UIScrollView, pageIndex: Int) -> AukPage? {
43 | let views = aukPages(scrollView)
44 | if views.count < pageIndex + 1 { return nil }
45 | return views[pageIndex]
46 | }
47 |
48 | /**
49 |
50 | - returns: The number of images on the given page. A page can show a placeholder image and a normal image on top.
51 |
52 | */
53 | func numberOfImagesOnPage(_ scrollView: UIScrollView, pageIndex: Int) -> Int {
54 | guard let view = aukPage(scrollView, pageIndex: pageIndex) else { return 123 }
55 | let imgesViews = view.subviews.filter { $0 is UIImageView }.map { $0 as! UIImageView }
56 |
57 | return imgesViews.filter { $0.image != nil }.count
58 | }
59 |
60 | /**
61 |
62 | - returns: The first image view form the AukPage with given index. A page can show a placeholder image and a normal image on top.
63 |
64 | */
65 | func firstAukImageView(_ scrollView: UIScrollView, pageIndex: Int) -> UIImageView? {
66 | if let view = aukPage(scrollView, pageIndex: pageIndex) {
67 | return view.subviews.filter { $0 is UIImageView }.map { $0 as! UIImageView }.first
68 | }
69 |
70 | return nil
71 | }
72 |
73 | /**
74 |
75 | - returns: The second image view form the AukPage with given index. A page can show a placeholder image and a normal image on top.
76 |
77 | */
78 | func secondAukImageView(_ scrollView: UIScrollView, pageIndex: Int) -> UIImageView? {
79 | if let view = aukPage(scrollView, pageIndex: pageIndex) {
80 | return view.subviews.filter { $0 is UIImageView }.map { $0 as! UIImageView }[1]
81 | }
82 |
83 | return nil
84 | }
85 |
86 | /**
87 |
88 | - returns: The first image the TheAukPage with given index. A page can show a placeholder image and a normal image on top.
89 |
90 | */
91 | func firstAukImage(_ scrollView: UIScrollView, pageIndex: Int) -> UIImage? {
92 | return firstAukImageView(scrollView, pageIndex: pageIndex)?.image
93 | }
94 |
95 | /**
96 |
97 | - returns: The width of the first image the TheAukPage with given index.
98 |
99 | */
100 | func firstAukImageWidth(_ scrollView: UIScrollView, pageIndex: Int) -> CGFloat {
101 | return firstAukImage(scrollView, pageIndex: pageIndex)!.size.width
102 | }
103 |
104 | /**
105 |
106 | - returns: The second image the TheAukPage with given index. A page can show a placeholder image and a normal image on top.
107 |
108 | */
109 | func secondAukImage(_ scrollView: UIScrollView, pageIndex: Int) -> UIImage? {
110 | return secondAukImageView(scrollView, pageIndex: pageIndex)?.image
111 | }
112 |
113 | /**
114 |
115 | - returns: The width of the second image the TheAukPage with given index.
116 |
117 | */
118 | func secondAukImageWidth(_ scrollView: UIScrollView, pageIndex: Int) -> CGFloat {
119 | return secondAukImage(scrollView, pageIndex: pageIndex)!.size.width
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/AukTests/AukScrollToTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Auk
3 |
4 | class AukScrollToTests: XCTestCase {
5 | var scrollView: UIScrollView!
6 |
7 | override func setUp() {
8 | super.setUp()
9 |
10 | scrollView = UIScrollView()
11 |
12 | // Set scroll view size
13 | let size = CGSize(width: 120, height: 90)
14 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
15 | }
16 |
17 | // MARK: - Content offset for page
18 |
19 | func testContentOffsetForPage() {
20 | var result = AukScrollTo.contentOffsetForPage(atIndex: 0, pageWidth: 120, numberOfPages: 3,
21 | scrollView: scrollView)
22 |
23 | XCTAssertEqual(0, result)
24 |
25 | result = AukScrollTo.contentOffsetForPage(atIndex: 0, pageWidth: 120, numberOfPages: 0,
26 | scrollView: scrollView)
27 |
28 | XCTAssertEqual(0, result)
29 |
30 | result = AukScrollTo.contentOffsetForPage(atIndex: 2, pageWidth: 120, numberOfPages: 3,
31 | scrollView: scrollView)
32 |
33 | XCTAssertEqual(240, result)
34 | }
35 |
36 | func testContentOffsetForPage_OverscrolledToRight() {
37 | let result = AukScrollTo.contentOffsetForPage(atIndex: 3, pageWidth: 120, numberOfPages: 3,
38 | scrollView: scrollView)
39 |
40 | XCTAssertEqual(240, result)
41 | }
42 |
43 | func testContentOffsetForPage_OverscrolledToLeft() {
44 | let result = AukScrollTo.contentOffsetForPage(atIndex: -1, pageWidth: 120, numberOfPages: 2,
45 | scrollView: scrollView)
46 |
47 | XCTAssertEqual(0, result)
48 | }
49 |
50 | func testContentOffsetForPage_rightToLeft() {
51 | if #available(iOS 9.0, *) {
52 | scrollView.semanticContentAttribute = .forceRightToLeft
53 |
54 | var result = AukScrollTo.contentOffsetForPage(atIndex: 0, pageWidth: 120, numberOfPages: 3,
55 | scrollView: scrollView)
56 |
57 | XCTAssertEqual(240, result)
58 |
59 | result = AukScrollTo.contentOffsetForPage(atIndex: 0, pageWidth: 120, numberOfPages: 2,
60 | scrollView: scrollView)
61 |
62 | XCTAssertEqual(120, result)
63 |
64 | result = AukScrollTo.contentOffsetForPage(atIndex: 0, pageWidth: 120, numberOfPages: 0,
65 | scrollView: scrollView)
66 |
67 | XCTAssertEqual(0, result)
68 |
69 | result = AukScrollTo.contentOffsetForPage(atIndex: 1, pageWidth: 120, numberOfPages: 3,
70 | scrollView: scrollView)
71 |
72 | XCTAssertEqual(120, result)
73 |
74 | result = AukScrollTo.contentOffsetForPage(atIndex: 2, pageWidth: 120, numberOfPages: 3,
75 | scrollView: scrollView)
76 |
77 | XCTAssertEqual(0, result)
78 | }
79 | }
80 |
81 | func testContentOffsetForPage_OverscrolledToRight_rightToLeft() {
82 | if #available(iOS 9.0, *) {
83 | scrollView.semanticContentAttribute = .forceRightToLeft
84 |
85 | let result = AukScrollTo.contentOffsetForPage(atIndex: 3, pageWidth: 120, numberOfPages: 3,
86 | scrollView: scrollView)
87 |
88 | XCTAssertEqual(0, result)
89 | }
90 | }
91 |
92 | func testContentOffsetForPage_OverscrolledToLeft_rightToLeft() {
93 | if #available(iOS 9.0, *) {
94 | scrollView.semanticContentAttribute = .forceRightToLeft
95 |
96 | let result = AukScrollTo.contentOffsetForPage(atIndex: -1, pageWidth: 120, numberOfPages: 2,
97 | scrollView: scrollView)
98 |
99 | XCTAssertEqual(120, result)
100 | }
101 | }
102 |
103 | // MARK: - Scroll to
104 |
105 | func testScrollTo() {
106 | AukScrollTo.scrollToPage(scrollView, atIndex: 1, pageWidth: 120, animated: false, numberOfPages: 2)
107 |
108 | XCTAssertEqual(120, scrollView.contentOffset.x)
109 | }
110 |
111 | func testScrollTo_rightToLeft() {
112 | if #available(iOS 9.0, *) {
113 | scrollView.semanticContentAttribute = .forceRightToLeft
114 |
115 | AukScrollTo.scrollToPage(scrollView, atIndex: 1, pageWidth: 120, animated: false, numberOfPages: 2)
116 |
117 | XCTAssertEqual(0, scrollView.contentOffset.x)
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Auk/AukPage.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /// The view for an individual page of the scroll view containing an image.
4 | final class AukPage: UIView {
5 |
6 | // Image view for showing a placeholder image while remote image is being downloaded.
7 | // The view is only created when a placeholder image is specified in settings.
8 | weak var placeholderImageView: UIImageView?
9 |
10 | // Image view for showing local and remote images
11 | weak var imageView: UIImageView?
12 |
13 | // Contains a URL for the remote image, if any.
14 | var remoteImage: AukRemoteImage?
15 |
16 | /**
17 |
18 | Shows an image.
19 |
20 | - parameter image: The image to be shown
21 | - parameter settings: Auk settings.
22 |
23 | */
24 | func show(image: UIImage, settings: AukSettings) {
25 | imageView = createAndLayoutImageView(settings)
26 | imageView?.image = image
27 | }
28 |
29 | /**
30 |
31 | Shows a remote image. The image download stars if/when the page becomes visible to the user.
32 |
33 | - parameter url: The URL to the image to be displayed.
34 | - parameter settings: Auk settings.
35 |
36 | */
37 | func show(url: String, settings: AukSettings) {
38 | if settings.placeholderImage != nil {
39 | placeholderImageView = createAndLayoutImageView(settings)
40 | }
41 |
42 | imageView = createAndLayoutImageView(settings)
43 |
44 | if let imageView = imageView {
45 | remoteImage = AukRemoteImage()
46 | remoteImage?.setup(url, imageView: imageView, placeholderImageView: placeholderImageView,
47 | settings: settings)
48 | }
49 | }
50 |
51 | /**
52 |
53 | Called when the page is currently visible to user which triggers the image download. The function is called frequently each time scroll view's content offset is changed.
54 |
55 | */
56 | func visibleNow(_ settings: AukSettings) {
57 | remoteImage?.downloadImage(settings)
58 | }
59 |
60 | /**
61 |
62 | Called when the page is currently not visible to user which cancels the image download. The method called frequently each time scroll view's content offset is changed and the page is out of sight.
63 |
64 | */
65 | func outOfSightNow() {
66 | remoteImage?.cancelDownload()
67 | }
68 |
69 | /// Removes image views.
70 | func removeImageViews() {
71 | placeholderImageView?.removeFromSuperview()
72 | placeholderImageView = nil
73 |
74 | imageView?.removeFromSuperview()
75 | imageView = nil
76 | }
77 |
78 | /**
79 |
80 | Prepares the page view for reuse. Clears current content from the page and stops download.
81 |
82 | */
83 | func prepareForReuse() {
84 | removeImageViews()
85 | remoteImage?.cancelDownload()
86 | remoteImage = nil
87 | }
88 |
89 | /**
90 |
91 | Create and layout the remote image view.
92 |
93 | - parameter settings: Auk settings.
94 |
95 | */
96 | func createAndLayoutImageView(_ settings: AukSettings) -> UIImageView {
97 | let newImageView = AukPage.createImageView(settings)
98 | addSubview(newImageView)
99 | AukPage.layoutImageView(newImageView, superview: self)
100 | return newImageView
101 | }
102 |
103 | private static func createImageView(_ settings: AukSettings) -> UIImageView {
104 | let newImageView = UIImageView()
105 | newImageView.contentMode = settings.contentMode
106 | return newImageView
107 | }
108 |
109 | /**
110 |
111 | Creates Auto Layout constrains for the image view.
112 |
113 | - parameter imageView: Image view that is used to create Auto Layout constraints.
114 |
115 | */
116 | private static func layoutImageView(_ imageView: UIImageView, superview: UIView) {
117 | imageView.translatesAutoresizingMaskIntoConstraints = false
118 |
119 | iiAutolayoutConstraints.fillParent(imageView, parentView: superview, margin: 0, vertically: false)
120 | iiAutolayoutConstraints.fillParent(imageView, parentView: superview, margin: 0, vertically: true)
121 | }
122 |
123 | func makeAccessible(_ accessibilityLabel: String?) {
124 | isAccessibilityElement = true
125 | accessibilityTraits = UIAccessibilityTraits.image
126 | self.accessibilityLabel = accessibilityLabel
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Auk.xcodeproj/xcshareddata/xcschemes/TheAukTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
78 |
79 |
85 |
86 |
87 |
88 |
89 |
90 |
96 |
97 |
99 |
100 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/Auk.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
53 |
54 |
55 |
56 |
57 |
58 |
68 |
70 |
76 |
77 |
78 |
79 |
80 |
81 |
87 |
89 |
95 |
96 |
97 |
98 |
100 |
101 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/Auk/AukPageIndicatorContainer.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /// View containing a UIPageControl object that shows the dots for present pages.
4 | final class AukPageIndicatorContainer: UIView {
5 |
6 | deinit {
7 | pageControl?.removeTarget(self, action: #selector(AukPageIndicatorContainer.didTapPageControl(_:)),
8 | for: UIControl.Event.valueChanged)
9 | }
10 |
11 | var didTapPageControlCallback: ((Int)->())?
12 |
13 | var pageControl: UIPageControl? {
14 | get {
15 | if subviews.count == 0 { return nil }
16 | return subviews[0] as? UIPageControl
17 | }
18 | }
19 |
20 | // Layouts the view, creates and layouts the page control
21 | func setup(_ settings: AukSettings, scrollView: UIScrollView) {
22 | styleContainer(settings)
23 | AukPageIndicatorContainer.layoutContainer(self, settings: settings, scrollView: scrollView)
24 |
25 | let pageControl = createPageControl(settings)
26 | AukPageIndicatorContainer.layoutPageControl(pageControl, superview: self, settings: settings)
27 |
28 | updateVisibility()
29 | }
30 |
31 | // Update the number of pages showing in the page control
32 | func updateNumberOfPages(_ numberOfPages: Int) {
33 | pageControl?.numberOfPages = numberOfPages
34 | updateVisibility()
35 | }
36 |
37 | // Update the current page in the page control
38 | func updateCurrentPage(_ currentPageIndex: Int) {
39 | pageControl?.currentPage = currentPageIndex
40 | }
41 |
42 | private func styleContainer(_ settings: AukSettings) {
43 | backgroundColor = settings.pageControl.backgroundColor
44 | layer.cornerRadius = CGFloat(settings.pageControl.cornerRadius)
45 | }
46 |
47 | private static func layoutContainer(_ pageIndicatorContainer: AukPageIndicatorContainer,
48 | settings: AukSettings, scrollView: UIScrollView) {
49 |
50 | if let superview = pageIndicatorContainer.superview {
51 | pageIndicatorContainer.translatesAutoresizingMaskIntoConstraints = false
52 |
53 | // Align bottom of the page view indicator with the bottom of the scroll view
54 | iiAutolayoutConstraints.alignSameAttributes(pageIndicatorContainer, toItem: scrollView,
55 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.bottom,
56 | margin: CGFloat(-settings.pageControl.marginToScrollViewBottom))
57 |
58 | // Center the page view indicator horizontally in relation to the scroll view
59 | iiAutolayoutConstraints.alignSameAttributes(pageIndicatorContainer, toItem: scrollView,
60 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.centerX, margin: 0)
61 | }
62 | }
63 |
64 | private func createPageControl(_ settings: AukSettings) -> UIPageControl {
65 | let pageControl = UIPageControl()
66 |
67 | if #available(*, iOS 9.0) {
68 | // iOS 9+
69 | } else {
70 | // When using right-to-left language, flip the page control horizontally in iOS 8 and earlier.
71 | // That will make it highlight the rightmost dot for the first page.
72 | if RightToLeft.isRightToLeft(self) {
73 | pageControl.transform = CGAffineTransform(scaleX: -1, y: 1)
74 | }
75 | }
76 |
77 | pageControl.addTarget(self, action: #selector(AukPageIndicatorContainer.didTapPageControl(_:)),
78 | for: UIControl.Event.valueChanged)
79 |
80 | pageControl.pageIndicatorTintColor = settings.pageControl.pageIndicatorTintColor
81 | pageControl.currentPageIndicatorTintColor = settings.pageControl.currentPageIndicatorTintColor
82 |
83 | addSubview(pageControl)
84 | return pageControl
85 | }
86 |
87 | @objc func didTapPageControl(_ control: UIPageControl) {
88 | if let currentPage = pageControl?.currentPage {
89 | didTapPageControlCallback?(currentPage)
90 | }
91 | }
92 |
93 | private static func layoutPageControl(_ pageControl: UIPageControl, superview: UIView,
94 | settings: AukSettings) {
95 |
96 | pageControl.translatesAutoresizingMaskIntoConstraints = false
97 |
98 | iiAutolayoutConstraints.fillParent(pageControl, parentView: superview,
99 | margin: settings.pageControl.innerPadding.width, vertically: false)
100 |
101 | iiAutolayoutConstraints.fillParent(pageControl, parentView: superview,
102 | margin: settings.pageControl.innerPadding.height, vertically: true)
103 | }
104 |
105 | private func updateVisibility() {
106 | if let pageControl = pageControl {
107 | self.isHidden = pageControl.numberOfPages < 2
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Auk.xcodeproj/xcshareddata/xcschemes/GreatAuk.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 |
67 |
68 |
69 |
70 |
71 |
72 |
82 |
83 |
89 |
90 |
91 |
92 |
93 |
94 |
100 |
101 |
107 |
108 |
109 |
110 |
112 |
113 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/AukTests/AukScrollViewContentTests.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import XCTest
3 | @testable import Auk
4 |
5 | class AukScrollViewContentTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | let settings = AukSettings()
9 | var fakeAnimator: iiFakeAnimator!
10 |
11 | override func setUp() {
12 | super.setUp()
13 |
14 | scrollView = UIScrollView()
15 |
16 | // Use fake animator
17 | fakeAnimator = iiFakeAnimator()
18 | iiAnimator.currentAnimator = fakeAnimator
19 | }
20 |
21 | override func tearDown() {
22 | super.tearDown()
23 |
24 | iiAnimator.currentAnimator = nil // Remove the fake animator
25 | }
26 |
27 | func testAukPages() {
28 | let aukView1 = AukPage()
29 | let aukView2 = AukPage()
30 |
31 | scrollView.addSubview(aukView1)
32 | scrollView.addSubview(aukView2)
33 |
34 | let pages = AukScrollViewContent.aukPages(scrollView)
35 |
36 | XCTAssertEqual(2, pages.count)
37 | XCTAssert(pages[0] === aukView1)
38 | XCTAssert(pages[1] === aukView2)
39 | }
40 |
41 | // MARK: - Layout
42 | // -----------------
43 |
44 | func testLayout() {
45 | scrollView.bounds.size = CGSize(width: 180, height: 120)
46 |
47 | let aukView1 = AukPage()
48 | let aukView2 = AukPage()
49 | let aukView3 = AukPage()
50 |
51 | scrollView.addSubview(aukView1)
52 | scrollView.addSubview(aukView2)
53 | scrollView.addSubview(aukView3)
54 |
55 | AukScrollViewContent.layout(scrollView)
56 |
57 | scrollView.layoutIfNeeded()
58 |
59 | // Check content size
60 | // -------------
61 |
62 | XCTAssertEqual(CGSize(width: 540, height: 120), scrollView.contentSize)
63 |
64 | // View 1
65 | // -------------
66 |
67 | XCTAssertEqual(CGPoint(x: 0, y: 0), aukView1.frame.origin)
68 | XCTAssertEqual(CGSize(width: 180, height: 120), aukView1.frame.size)
69 |
70 | // View 2
71 | // -------------
72 |
73 | XCTAssertEqual(CGPoint(x: 180, y: 0), aukView2.frame.origin)
74 | XCTAssertEqual(CGSize(width: 180, height: 120), aukView2.frame.size)
75 |
76 | // View 3
77 | // -------------
78 |
79 | XCTAssertEqual(CGPoint(x: 360, y: 0), aukView3.frame.origin)
80 | XCTAssertEqual(CGSize(width: 180, height: 120), aukView3.frame.size)
81 | }
82 |
83 | func testLayout_callCompletionFunction() {
84 | var didCallCompletion = false
85 |
86 | AukScrollViewContent.layout(scrollView, animated: false, animationDurationInSeconds: 0, completion: {
87 | didCallCompletion = true
88 | })
89 |
90 | assert(didCallCompletion)
91 | }
92 |
93 | func testLayout_animated() {
94 | scrollView.bounds.size = CGSize(width: 180, height: 120)
95 | let aukView1 = AukPage()
96 | let aukView2 = AukPage()
97 | let aukView3 = AukPage()
98 |
99 | scrollView.addSubview(aukView1)
100 | scrollView.addSubview(aukView2)
101 | scrollView.addSubview(aukView3)
102 |
103 | var didCallCompletion = false
104 |
105 | AukScrollViewContent.layout(scrollView, animated: true, animationDurationInSeconds: 120, completion: {
106 | didCallCompletion = true
107 | }
108 | )
109 |
110 | XCTAssertEqual(1, fakeAnimator.testParameters.count)
111 | XCTAssertEqual(120, fakeAnimator.testParameters[0].duration)
112 |
113 | // Animation
114 | XCTAssertEqual(CGSize(width: 0, height: 0), scrollView.contentSize)
115 | fakeAnimator.testParameters[0].animation()
116 | XCTAssertEqual(CGSize(width: 540, height: 120), scrollView.contentSize)
117 |
118 | // Completion
119 | XCTAssertFalse(didCallCompletion)
120 | fakeAnimator.testParameters[0].completion?(true)
121 | XCTAssert(didCallCompletion)
122 | }
123 |
124 | // MARK: - PageAt
125 | // -----------------
126 |
127 | func testPageAt() {
128 | let aukView1 = AukPage()
129 | let aukView2 = AukPage()
130 |
131 | scrollView.addSubview(aukView1)
132 | scrollView.addSubview(aukView2)
133 |
134 | var result = AukScrollViewContent.page(atIndex: 0, scrollView: scrollView)
135 | XCTAssert(result === aukView1)
136 |
137 | result = AukScrollViewContent.page(atIndex: 1, scrollView: scrollView)
138 | XCTAssert(result === aukView2)
139 | }
140 |
141 | func testPageAt_noPages() {
142 | let result = AukScrollViewContent.page(atIndex: 0, scrollView: scrollView)
143 | XCTAssertNil(result)
144 | }
145 |
146 | func testPageAt_indexGreaterThanExist() {
147 | let aukView1 = AukPage()
148 | let aukView2 = AukPage()
149 |
150 | scrollView.addSubview(aukView1)
151 | scrollView.addSubview(aukView2)
152 |
153 | let result = AukScrollViewContent.page(atIndex: 2, scrollView: scrollView)
154 | XCTAssertNil(result)
155 | }
156 |
157 | func testPageAt_indexLessThanExist() {
158 | let aukView1 = AukPage()
159 | let aukView2 = AukPage()
160 |
161 | scrollView.addSubview(aukView1)
162 | scrollView.addSubview(aukView2)
163 |
164 | let result = AukScrollViewContent.page(atIndex: -1, scrollView: scrollView)
165 | XCTAssertNil(result)
166 | }
167 |
168 | }
169 |
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceShowLocalImageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import UIKit
3 | @testable import Auk
4 |
5 | class AukInterfaceShowLocalImageTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | var auk: Auk!
9 |
10 | override func setUp() {
11 | super.setUp()
12 |
13 | scrollView = UIScrollView()
14 |
15 | // Set scroll view size
16 | let size = CGSize(width: 120, height: 90)
17 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
18 |
19 | auk = Auk(scrollView: scrollView)
20 | }
21 |
22 | func testSetupIsCalled() {
23 | let image = createImage96px()
24 | auk.show(image: image)
25 |
26 | XCTAssertFalse(scrollView.showsHorizontalScrollIndicator)
27 | }
28 |
29 | func testShowLocalImage() {
30 | let image = createImage96px()
31 | auk.show(image: image)
32 |
33 | XCTAssertEqual(1, aukPages(scrollView).count)
34 | XCTAssertEqual(96, firstAukImageWidth(scrollView, pageIndex: 0))
35 | }
36 |
37 | func testShowLocalImage_layoutSubviews() {
38 | let image1 = createImage96px()
39 | auk.show(image: image1)
40 |
41 | let image2 = createImage67px()
42 | auk.show(image: image2)
43 |
44 | scrollView.layoutIfNeeded()
45 |
46 | // Check content size
47 | // -------------
48 |
49 | XCTAssertEqual(CGSize(width: 240, height: 90), scrollView.contentSize)
50 |
51 | // View 1
52 | // -------------
53 |
54 | let aukView1 = aukPages(scrollView)[0]
55 | XCTAssertEqual(CGPoint(x: 0, y: 0), aukView1.frame.origin)
56 | XCTAssertEqual(CGSize(width: 120, height: 90), aukView1.frame.size)
57 |
58 | // View 2
59 | // -------------
60 |
61 | let aukView2 = aukPages(scrollView)[1]
62 | XCTAssertEqual(CGPoint(x: 120, y: 0), aukView2.frame.origin)
63 | XCTAssertEqual(CGSize(width: 120, height: 90), aukView2.frame.size)
64 | }
65 |
66 | func testShowLocalImage_layoutSubviews_rightToLeft() {
67 | if #available(iOS 9.0, *) {
68 | scrollView.semanticContentAttribute = .forceRightToLeft
69 |
70 | let image1 = createImage96px()
71 | auk.show(image: image1)
72 |
73 | let image2 = createImage67px()
74 | auk.show(image: image2)
75 |
76 | scrollView.layoutIfNeeded()
77 |
78 | // Check content size
79 | // -------------
80 |
81 | XCTAssertEqual(CGSize(width: 240, height: 90), scrollView.contentSize)
82 |
83 | // View 1
84 | // -------------
85 |
86 | let aukView1 = aukPages(scrollView)[0]
87 | XCTAssertEqual(CGPoint(x: 120, y: 0), aukView1.frame.origin)
88 | XCTAssertEqual(CGSize(width: 120, height: 90), aukView1.frame.size)
89 | XCTAssertEqual(96, firstAukImageWidth(scrollView, pageIndex: 0))
90 |
91 | // View 2
92 | // -------------
93 |
94 | let aukView2 = aukPages(scrollView)[1]
95 | XCTAssertEqual(CGPoint(x: 0, y: 0), aukView2.frame.origin)
96 | XCTAssertEqual(CGSize(width: 120, height: 90), aukView2.frame.size)
97 | XCTAssertEqual(67, firstAukImageWidth(scrollView, pageIndex: 1))
98 |
99 | }
100 | }
101 |
102 | func testShowLocalImage_contentOffset() {
103 | XCTAssertEqual(0, scrollView.contentOffset.x)
104 |
105 | let image1 = createImage96px()
106 | auk.show(image: image1)
107 |
108 | XCTAssertEqual(0, scrollView.contentOffset.x)
109 |
110 | let image2 = createImage67px()
111 | auk.show(image: image2)
112 |
113 | XCTAssertEqual(0, scrollView.contentOffset.x)
114 | }
115 |
116 | func testShowLocalImage_contentOffset_rightToLeft() {
117 | if #available(iOS 9.0, *) {
118 | scrollView.semanticContentAttribute = .forceRightToLeft
119 |
120 | XCTAssertEqual(0, scrollView.contentOffset.x)
121 |
122 | let image1 = createImage96px()
123 | auk.show(image: image1)
124 | XCTAssertEqual(0, scrollView.contentOffset.x)
125 |
126 | let image2 = createImage67px()
127 | auk.show(image: image2)
128 | XCTAssertEqual(120, scrollView.contentOffset.x)
129 |
130 | let image3 = createImage35px()
131 | auk.show(image: image3)
132 | XCTAssertEqual(240, scrollView.contentOffset.x)
133 | }
134 | }
135 |
136 | // MARK: - Accessibility
137 |
138 | func testCreateAccessiblePageView_withLabel() {
139 | let image = createImage96px()
140 | auk.show(image: image, accessibilityLabel: "White knight riding a wooden horse on wheels.")
141 |
142 | let page = aukPage(scrollView, pageIndex: 0)!
143 |
144 | XCTAssert(page.isAccessibilityElement)
145 | XCTAssertEqual(page.accessibilityTraits, UIAccessibilityTraits.image)
146 | XCTAssertEqual("White knight riding a wooden horse on wheels.", page.accessibilityLabel!)
147 | }
148 |
149 | func testCreateAccessiblePageView_withoutLabel() {
150 | let image = createImage96px()
151 | auk.show(image: image)
152 |
153 | let page = aukPage(scrollView, pageIndex: 0)!
154 |
155 | XCTAssert(page.isAccessibilityElement)
156 | XCTAssertEqual(page.accessibilityTraits, UIAccessibilityTraits.image)
157 | XCTAssert(page.accessibilityLabel == nil)
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/Auk/Utils/iiAutolayoutConstraints.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Collection of shortcuts to create autolayout constraints.
3 | //
4 |
5 | import UIKit
6 |
7 | class iiAutolayoutConstraints {
8 | class func fillParent(_ view: UIView, parentView: UIView, margin: CGFloat = 0, vertically: Bool = false) {
9 | var marginFormat = ""
10 |
11 | if margin != 0 {
12 | marginFormat = "-(\(margin))-"
13 | }
14 |
15 | var format = "|\(marginFormat)[view]\(marginFormat)|"
16 |
17 | if vertically {
18 | format = "V:" + format
19 | }
20 |
21 | let constraints = NSLayoutConstraint.constraints(withVisualFormat: format,
22 | options: [], metrics: nil,
23 | views: ["view": view])
24 |
25 | parentView.addConstraints(constraints)
26 | }
27 |
28 | @discardableResult
29 | class func alignSameAttributes(_ item: AnyObject, toItem: AnyObject,
30 | constraintContainer: UIView, attribute: NSLayoutConstraint.Attribute, margin: CGFloat = 0) -> [NSLayoutConstraint] {
31 |
32 | let constraint = NSLayoutConstraint(
33 | item: item,
34 | attribute: attribute,
35 | relatedBy: NSLayoutConstraint.Relation.equal,
36 | toItem: toItem,
37 | attribute: attribute,
38 | multiplier: 1,
39 | constant: margin)
40 |
41 | constraintContainer.addConstraint(constraint)
42 |
43 | return [constraint]
44 | }
45 |
46 | class func equalWidth(_ viewOne: UIView, viewTwo: UIView, constraintContainer: UIView) -> [NSLayoutConstraint] {
47 |
48 | return equalWidthOrHeight(viewOne, viewTwo: viewTwo, constraintContainer: constraintContainer, isHeight: false)
49 | }
50 |
51 | // MARK: - Equal height and width
52 |
53 | class func equalHeight(_ viewOne: UIView, viewTwo: UIView, constraintContainer: UIView) -> [NSLayoutConstraint] {
54 |
55 | return equalWidthOrHeight(viewOne, viewTwo: viewTwo, constraintContainer: constraintContainer, isHeight: true)
56 | }
57 |
58 | @discardableResult
59 | class func equalSize(_ viewOne: UIView, viewTwo: UIView, constraintContainer: UIView) -> [NSLayoutConstraint] {
60 |
61 | var constraints = equalWidthOrHeight(viewOne, viewTwo: viewTwo, constraintContainer: constraintContainer, isHeight: false)
62 |
63 | constraints += equalWidthOrHeight(viewOne, viewTwo: viewTwo, constraintContainer: constraintContainer, isHeight: true)
64 |
65 | return constraints
66 | }
67 |
68 | class func equalWidthOrHeight(_ viewOne: UIView, viewTwo: UIView, constraintContainer: UIView,
69 | isHeight: Bool) -> [NSLayoutConstraint] {
70 |
71 | var prefix = ""
72 |
73 | if isHeight { prefix = "V:" }
74 |
75 | let constraints = NSLayoutConstraint.constraints(withVisualFormat: "\(prefix)[viewOne(==viewTwo)]",
76 | options: [], metrics: nil,
77 | views: ["viewOne": viewOne, "viewTwo": viewTwo])
78 |
79 | constraintContainer.addConstraints(constraints)
80 |
81 | return constraints
82 | }
83 |
84 | // MARK: - Align view next to each other
85 |
86 | @discardableResult
87 | class func viewsNextToEachOther(_ views: [UIView],
88 | constraintContainer: UIView, margin: CGFloat = 0,
89 | vertically: Bool = false) -> [NSLayoutConstraint] {
90 |
91 | if views.count < 2 { return [] }
92 |
93 | var constraints = [NSLayoutConstraint]()
94 |
95 | for (index, view) in views.enumerated() {
96 | if index >= views.count - 1 { break }
97 |
98 | let viewTwo = views[index + 1]
99 |
100 | constraints += twoViewsNextToEachOther(view, viewTwo: viewTwo,
101 | constraintContainer: constraintContainer, margin: margin, vertically: vertically)
102 | }
103 |
104 | return constraints
105 | }
106 |
107 | class func twoViewsNextToEachOther(_ viewOne: UIView, viewTwo: UIView,
108 | constraintContainer: UIView, margin: CGFloat = 0,
109 | vertically: Bool = false) -> [NSLayoutConstraint] {
110 |
111 | var marginFormat = ""
112 |
113 | if margin != 0 {
114 | marginFormat = "-\(margin)-"
115 | }
116 |
117 | var format = "[viewOne]\(marginFormat)[viewTwo]"
118 |
119 | if vertically {
120 | format = "V:" + format
121 | }
122 |
123 | let constraints = NSLayoutConstraint.constraints(withVisualFormat: format,
124 | options: [], metrics: nil,
125 | views: [ "viewOne": viewOne, "viewTwo": viewTwo ])
126 |
127 | constraintContainer.addConstraints(constraints)
128 |
129 | return constraints
130 | }
131 |
132 | @discardableResult
133 | class func height(_ view: UIView, value: CGFloat) -> [NSLayoutConstraint] {
134 | return widthOrHeight(view, value: value, isHeight: true)
135 | }
136 |
137 | @discardableResult
138 | class func width(_ view: UIView, value: CGFloat) -> [NSLayoutConstraint] {
139 | return widthOrHeight(view, value: value, isHeight: false)
140 | }
141 |
142 | class func widthOrHeight(_ view: UIView, value: CGFloat, isHeight: Bool) -> [NSLayoutConstraint] {
143 |
144 | let layoutAttribute = isHeight ? NSLayoutConstraint.Attribute.height : NSLayoutConstraint.Attribute.width
145 |
146 | let constraint = NSLayoutConstraint(
147 | item: view,
148 | attribute: layoutAttribute,
149 | relatedBy: NSLayoutConstraint.Relation.equal,
150 | toItem: nil,
151 | attribute: NSLayoutConstraint.Attribute.notAnAttribute,
152 | multiplier: 1,
153 | constant: value)
154 |
155 | view.addConstraint(constraint)
156 |
157 | return [constraint]
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/AukTests/AukRemoteImageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import moa
3 | @testable import Auk
4 |
5 | class AukRemoteImageTests: XCTestCase {
6 |
7 | var obj: AukRemoteImage!
8 | var imageView: UIImageView!
9 | var settings: AukSettings!
10 |
11 | override func setUp() {
12 | super.setUp()
13 |
14 | imageView = UIImageView()
15 | obj = AukRemoteImage()
16 | settings = AukSettings()
17 | }
18 |
19 | override func tearDown() {
20 | super.tearDown()
21 |
22 | MoaSimulator.clear()
23 | }
24 |
25 | // MARK: - Download image
26 |
27 | func testDownloadImage() {
28 | obj.setup("http://site.com/auk.jpg", imageView: imageView,
29 | placeholderImageView: nil, settings: settings)
30 |
31 | let simulator = MoaSimulator.simulate("auk.jpg")
32 |
33 | obj.downloadImage(settings)
34 |
35 | XCTAssertEqual(1, simulator.downloaders.count)
36 | XCTAssertEqual("http://site.com/auk.jpg", simulator.downloaders.first!.url)
37 |
38 | simulator.respondWithImage(createImage96px())
39 |
40 | XCTAssertEqual(96, imageView.image!.size.width)
41 | }
42 |
43 | func testDownloadImage_downloadOnlyOnce_whenCalledMultipleTimes() {
44 | obj.setup("http://site.com/auk.jpg", imageView: imageView,
45 | placeholderImageView: nil, settings: settings)
46 |
47 | let simulator = MoaSimulator.simulate("auk.jpg")
48 |
49 | obj.downloadImage(settings)
50 | obj.downloadImage(settings)
51 | obj.downloadImage(settings)
52 |
53 | simulator.respondWithImage(createImage96px())
54 |
55 | XCTAssertEqual(1, simulator.downloaders.count)
56 | }
57 |
58 | // MARK: - Cancel image download
59 |
60 | func testCancelDownload() {
61 | obj.setup("http://site.com/auk.jpg", imageView: imageView,
62 | placeholderImageView: nil, settings: settings)
63 |
64 | let simulator = MoaSimulator.simulate("auk.jpg")
65 |
66 | // Request image download
67 | imageView.moa.url = "http://site.com/auk.jpg"
68 |
69 | obj.cancelDownload()
70 |
71 | XCTAssert(simulator.downloaders.first!.cancelled)
72 | XCTAssert(imageView.moa.url == nil)
73 | }
74 |
75 | func testCancelDownload_andStartAgain() {
76 | obj.setup("http://site.com/auk.jpg", imageView: imageView,
77 | placeholderImageView: nil, settings: settings)
78 |
79 | let simulator = MoaSimulator.simulate("auk.jpg")
80 |
81 | // Request image download
82 | imageView.moa.url = "http://site.com/auk.jpg"
83 |
84 | obj.cancelDownload()
85 | obj.downloadImage(settings)
86 |
87 | XCTAssertEqual(2, simulator.downloaders.count)
88 | XCTAssertEqual("http://site.com/auk.jpg", simulator.downloaders.last!.url)
89 | XCTAssertFalse(simulator.downloaders.last!.cancelled)
90 |
91 | simulator.respondWithImage(createImage67px())
92 |
93 | XCTAssertEqual(67, imageView.image!.size.width)
94 | }
95 |
96 | func testDownloadImage_doNotDownloadImageThatHasBeenAlreadyDownloaded() {
97 | obj.setup("http://site.com/auk.jpg", imageView: imageView,
98 | placeholderImageView: nil, settings: settings)
99 |
100 | let simulator = MoaSimulator.simulate("auk.jpg")
101 |
102 | obj.downloadImage(settings)
103 |
104 | // Respond with image
105 | simulator.respondWithImage(createImage96px())
106 |
107 | // Call download again
108 | obj.downloadImage(settings)
109 |
110 | // Should not download the second time
111 | XCTAssertEqual(1, simulator.downloaders.count)
112 | }
113 |
114 | func testDownloadImage_doNotDownloadImageThatHasBeenAlreadyDownloadedAndCancelled() {
115 | obj.setup("http://site.com/auk.jpg", imageView: imageView,
116 | placeholderImageView: nil, settings: settings)
117 |
118 | let simulator = MoaSimulator.simulate("auk.jpg")
119 |
120 | obj.downloadImage(settings)
121 |
122 | // Respond with image
123 | simulator.respondWithImage(createImage96px())
124 |
125 | // Call cancelDownload (which does not actually cancel anything, because image has already been downloaded)
126 | obj.cancelDownload()
127 |
128 | // Call download again
129 | obj.downloadImage(settings)
130 |
131 | // Should not download the second time
132 | XCTAssertEqual(1, simulator.downloaders.count)
133 | }
134 |
135 | // MARK: - Did receive image
136 |
137 | func testDidReceiveImage_markDownloadAsFinished() {
138 | XCTAssertFalse(obj.didFinishDownload)
139 |
140 | obj.didReceiveImageAsync(UIImage(), settings: settings)
141 |
142 | XCTAssert(obj.didFinishDownload)
143 | }
144 |
145 | // MARK: - Show placeholder image before image is download
146 |
147 | func testShowPlaceholderImage() {
148 | settings.placeholderImage = createImage35px()
149 | let placeholderImageView = UIImageView()
150 |
151 | obj.setup("http://site.com/auk.jpg", imageView: imageView,
152 | placeholderImageView: placeholderImageView, settings: settings)
153 |
154 | // Show placeholder
155 | XCTAssertEqual(35, placeholderImageView.image!.size.width)
156 | }
157 |
158 | // MARK: - Show error image
159 |
160 | func testShowErrorImage() {
161 | settings.errorImage = createImage35px()
162 | let simulator = MoaSimulator.simulate("auk.jpg")
163 |
164 | let errorExpectation = expectation(description: "error expectation")
165 |
166 | obj.setup("http://site.com/auk.jpg", imageView: imageView, placeholderImageView: nil,
167 | settings: settings)
168 |
169 | // Request remote image
170 | obj.downloadImage(settings)
171 |
172 | simulator.respondWithError(nil, response: nil)
173 |
174 | iiQ.runAfterDelay(0.001) { errorExpectation.fulfill() }
175 | waitForExpectations(timeout: 1) { error in }
176 |
177 | // Show error image
178 | XCTAssertEqual(35, imageView.image!.size.width)
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceShowRemoteImageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import moa
3 | @testable import Auk
4 |
5 | class AukInterfaceShowRemoteImageTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | var auk: Auk!
9 |
10 | override func setUp() {
11 | super.setUp()
12 |
13 | scrollView = UIScrollView()
14 |
15 | // Set scroll view size
16 | let size = CGSize(width: 120, height: 90)
17 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
18 |
19 | auk = Auk(scrollView: scrollView)
20 | }
21 |
22 | override func tearDown() {
23 | super.tearDown()
24 |
25 | MoaSimulator.clear()
26 | }
27 |
28 | func testShowRemoteImage_setupIsCalled() {
29 | _ = MoaSimulator.simulate("site.com")
30 |
31 | auk.show(url: "http://site.com/image.png")
32 |
33 | XCTAssertFalse(scrollView.showsHorizontalScrollIndicator)
34 | }
35 |
36 | func testShowRemoteImage() {
37 | let simulator = MoaSimulator.simulate("auk.png")
38 |
39 | auk.show(url: "http://site.com/auk.png")
40 |
41 | XCTAssertEqual(1, simulator.downloaders.count)
42 | XCTAssertEqual("http://site.com/auk.png", simulator.downloaders.first!.url)
43 |
44 | scrollView.layoutIfNeeded()
45 |
46 | let image = createImage67px()
47 | simulator.respondWithImage(image)
48 |
49 | XCTAssertEqual(1, aukPages(scrollView).count)
50 |
51 | // Show remote image without placeholder image
52 | XCTAssertEqual(1, numberOfImagesOnPage(scrollView, pageIndex: 0))
53 |
54 | // Loads image
55 | XCTAssertEqual(67, firstAukImageWidth(scrollView, pageIndex: 0))
56 | }
57 |
58 | func testShowRemoteImageWithPlaceholder() {
59 | let simulator = MoaSimulator.simulate("auk.png")
60 |
61 | auk.settings.placeholderImage = createImage35px()
62 | auk.show(url: "http://site.com/auk.png")
63 |
64 | XCTAssertEqual(1, simulator.downloaders.count)
65 | XCTAssertEqual("http://site.com/auk.png", simulator.downloaders.first!.url)
66 |
67 | scrollView.layoutIfNeeded()
68 |
69 | // Show placeholder image
70 | XCTAssertEqual(35, firstAukImageWidth(scrollView, pageIndex: 0))
71 |
72 | let image = createImage67px()
73 | simulator.respondWithImage(image)
74 |
75 | XCTAssertEqual(1, aukPages(scrollView).count)
76 |
77 | // Shows a placeholder image and a remote image
78 | XCTAssertEqual(2, numberOfImagesOnPage(scrollView, pageIndex: 0))
79 |
80 | // Show placeholder image
81 | XCTAssertEqual(35, firstAukImageWidth(scrollView, pageIndex: 0))
82 |
83 | // Show remote image
84 | XCTAssertEqual(67, secondAukImage(scrollView, pageIndex: 0)!.size.width)
85 | }
86 |
87 | func testShowRemoteImage_showSecondImageWhenScrolled() {
88 | let simulator = MoaSimulator.simulate("site.com")
89 |
90 | // Add two remote images
91 | auk.show(url: "http://site.com/auk.png")
92 | auk.show(url: "http://site.com/moa.png")
93 |
94 | // The first image is requested at first
95 | XCTAssertEqual(1, simulator.downloaders.count)
96 | XCTAssertEqual("http://site.com/auk.png", simulator.downloaders.first!.url)
97 | XCTAssertFalse(simulator.downloaders.first!.cancelled)
98 |
99 | // Scroll to make the second image visible
100 | scrollView.contentOffset.x = 10
101 | scrollView.delegate?.scrollViewDidScroll?(scrollView)
102 |
103 | // The second image is requested
104 | XCTAssertEqual(2, simulator.downloaders.count)
105 | XCTAssertEqual("http://site.com/moa.png", simulator.downloaders.last!.url)
106 | XCTAssertFalse(simulator.downloaders.last!.cancelled)
107 |
108 | // Scroll to hide the first image
109 | scrollView.contentOffset.x = 120
110 | scrollView.delegate?.scrollViewDidScroll?(scrollView)
111 |
112 | // Download of first image is NOT cancelled yet because it is close (though not visible)
113 | XCTAssertFalse(simulator.downloaders.first!.cancelled)
114 |
115 | // Scroll more to cancel first image download
116 | scrollView.contentOffset.x = 180
117 | scrollView.delegate?.scrollViewDidScroll?(scrollView)
118 |
119 | // Download of first image is cancelled
120 | XCTAssert(simulator.downloaders.first!.cancelled)
121 | }
122 |
123 | func testShowRemoteImage_layoutSubviews() {
124 | _ = MoaSimulator.simulate("site.com")
125 |
126 | auk.show(url: "http://site.com/image1.png")
127 | auk.show(url: "http://site.com/image2.png")
128 |
129 | scrollView.layoutIfNeeded()
130 |
131 | // Check content size
132 | // -------------
133 |
134 | XCTAssertEqual(CGSize(width: 240, height: 90), scrollView.contentSize)
135 |
136 | // View 1
137 | // -------------
138 |
139 | let aukView1 = aukPages(scrollView)[0]
140 | XCTAssertEqual(CGPoint(x: 0, y: 0), aukView1.frame.origin)
141 | XCTAssertEqual(CGSize(width: 120, height: 90), aukView1.frame.size)
142 |
143 | // View 2
144 | // -------------
145 |
146 | let aukView2 = aukPages(scrollView)[1]
147 | XCTAssertEqual(CGPoint(x: 120, y: 0), aukView2.frame.origin)
148 | XCTAssertEqual(CGSize(width: 120, height: 90), aukView2.frame.size)
149 | }
150 |
151 | // MARK: - Accessibility
152 |
153 | func testCreateAccessiblePageView_withLabel() {
154 | auk.show(url: "http://site.com/image.png",
155 | accessibilityLabel: "White knight riding a wooden horse on wheels.")
156 |
157 | let page = aukPage(scrollView, pageIndex: 0)!
158 |
159 | XCTAssert(page.isAccessibilityElement)
160 | XCTAssertEqual(page.accessibilityTraits, UIAccessibilityTraits.image)
161 | XCTAssertEqual("White knight riding a wooden horse on wheels.", page.accessibilityLabel!)
162 | }
163 |
164 | func testCreateAccessiblePageView_withoutLabel() {
165 | auk.show(url: "http://site.com/image.png")
166 |
167 | let page = aukPage(scrollView, pageIndex: 0)!
168 |
169 | XCTAssert(page.isAccessibilityElement)
170 | XCTAssertEqual(page.accessibilityTraits, UIAccessibilityTraits.image)
171 | XCTAssert(page.accessibilityLabel == nil)
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/AukTests/AukPageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import moa
3 | @testable import Auk
4 |
5 | class AukPageTests: XCTestCase {
6 |
7 | var view: AukPage!
8 | var settings: AukSettings!
9 |
10 | override func setUp() {
11 | super.setUp()
12 | view = AukPage()
13 | settings = AukSettings()
14 | }
15 |
16 | override func tearDown() {
17 | super.tearDown()
18 |
19 | MoaSimulator.clear()
20 | }
21 |
22 | // MARK: - Show image
23 |
24 | func testShowImage() {
25 | let image = createImage67px()
26 |
27 | view.show(image: image, settings: settings)
28 |
29 | XCTAssertEqual(67, view.imageView!.image!.size.width)
30 | }
31 |
32 | func testShowImage_setup() {
33 | let image = createImage67px()
34 |
35 | view.show(image: image, settings: settings)
36 |
37 | XCTAssert(view.imageView != nil)
38 |
39 | // Do not create placeholder image view when showing local image
40 | XCTAssert(view.placeholderImageView == nil)
41 | }
42 |
43 | func testShowImage_useContentMode() {
44 | settings.contentMode = UIView.ContentMode.topRight
45 | let image = createImage67px()
46 | view.show(image: image, settings: settings)
47 | XCTAssertEqual(UIView.ContentMode.topRight.rawValue, view.imageView!.contentMode.rawValue)
48 | }
49 |
50 | func testShowImage_doNotcreatePlaceholderImage() {
51 | settings.placeholderImage = nil
52 | let image = createImage67px()
53 |
54 | view.show(image: image, settings: settings)
55 |
56 | XCTAssert(view.placeholderImageView == nil)
57 | }
58 |
59 | // MARK: - Show image by url
60 |
61 | func testShowUrl_useContentMode() {
62 | settings.contentMode = UIView.ContentMode.topRight
63 | view.show(url: "http://site.com/auk.jpg", settings: settings)
64 | XCTAssertEqual(UIView.ContentMode.topRight.rawValue, view.imageView!.contentMode.rawValue)
65 | }
66 |
67 | func testShowUrl() {
68 | view.show(url: "http://site.com/auk.jpg", settings: settings)
69 |
70 | XCTAssertEqual("http://site.com/auk.jpg", view.remoteImage!.url!)
71 | }
72 |
73 | func testShowUrl_setup() {
74 | view.show(url: "http://site.com/auk.jpg", settings: settings)
75 |
76 | XCTAssert(view.imageView != nil)
77 | }
78 |
79 | func testShowUrl_doNotcreatePlaceholderImage() {
80 | settings.placeholderImage = nil
81 | view.show(url: "http://site.com/auk.jpg", settings: settings)
82 |
83 | XCTAssert(view.placeholderImageView == nil)
84 | }
85 |
86 | func testShowUrl_createPlaceholderImage() {
87 | settings.placeholderImage = UIImage()
88 | view.show(url: "http://site.com/auk.jpg", settings: settings)
89 |
90 | XCTAssert(view.placeholderImageView != nil)
91 | }
92 |
93 | // MARK: - Visible now
94 |
95 | func testVisibleNow() {
96 | let simulator = MoaSimulator.simulate("auk.jpg")
97 | let imageView = UIImageView()
98 |
99 | view.remoteImage = AukRemoteImage()
100 | view.remoteImage?.setup("http://site.com/auk.jpg", imageView: imageView,
101 | placeholderImageView: nil, settings: settings)
102 |
103 | view.visibleNow(settings)
104 |
105 | XCTAssertEqual(1, simulator.downloaders.count)
106 | XCTAssertEqual("http://site.com/auk.jpg", simulator.downloaders.first!.url)
107 |
108 | let image = createImage35px()
109 | simulator.respondWithImage(image)
110 |
111 | XCTAssertEqual(35, imageView.image!.size.width)
112 | }
113 |
114 | func testVisibleNow_whenCalledTwiceDownloadOnlyOnce() {
115 | let simulator = MoaSimulator.simulate("auk.jpg")
116 | let imageView = UIImageView()
117 |
118 | view.remoteImage = AukRemoteImage()
119 | view.remoteImage?.setup("http://site.com/auk.jpg", imageView: imageView,
120 | placeholderImageView: nil, settings: settings)
121 |
122 | view.visibleNow(settings)
123 | view.visibleNow(settings)
124 |
125 | let image = createImage35px()
126 | simulator.respondWithImage(image)
127 |
128 | XCTAssertEqual(1, simulator.downloaders.count)
129 | }
130 |
131 | // MARK: - Out of sight now
132 |
133 | func testOutOfSightNow_cancelCurrentImageDownload() {
134 | let simulator = MoaSimulator.simulate("auk.jpg")
135 | let imageView = UIImageView()
136 | view.remoteImage = AukRemoteImage()
137 | view.remoteImage?.setup("http://site.com/auk.jpg", imageView: imageView,
138 | placeholderImageView: nil, settings: settings)
139 |
140 | // Request image download
141 | imageView.moa.url = "http://site.com/auk.jpg"
142 |
143 | XCTAssertEqual(1, simulator.downloaders.count)
144 | XCTAssertEqual("http://site.com/auk.jpg", simulator.downloaders.first!.url)
145 |
146 | view.outOfSightNow()
147 |
148 | XCTAssert(simulator.downloaders.first!.cancelled)
149 | XCTAssert(imageView.moa.url == nil)
150 | }
151 |
152 | // MARK: - Remove image views
153 |
154 | func testRemoveImage() {
155 | let image = createImage67px()
156 | view.show(image: image, settings: settings)
157 | view.removeImageViews()
158 |
159 | XCTAssertNil(view.imageView)
160 | }
161 |
162 | func testRemovePlaceholderImage() {
163 | settings.placeholderImage = UIImage()
164 | view.show(url: "http://site.com/auk.jpg", settings: settings)
165 | view.removeImageViews()
166 |
167 | XCTAssertNil(view.placeholderImageView)
168 | }
169 |
170 | // MARK: - Prepare for reuse
171 |
172 | func testPrepareForReuse_removesImageView() {
173 | settings.placeholderImage = UIImage()
174 | let image = createImage67px()
175 | view.show(image: image, settings: settings)
176 |
177 | view.prepareForReuse()
178 |
179 | XCTAssertNil(view.imageView)
180 | XCTAssertNil(view.placeholderImageView)
181 | }
182 |
183 | func testPrepareForReuse_cancelCurrentDownload() {
184 | let simulator = MoaSimulator.simulate("auk.jpg")
185 | let imageView = UIImageView()
186 | view.remoteImage = AukRemoteImage()
187 | view.remoteImage?.setup("http://site.com/auk.jpg", imageView: imageView,
188 | placeholderImageView: nil, settings: settings)
189 |
190 | // Request image download
191 | imageView.moa.url = "http://site.com/auk.jpg"
192 |
193 | XCTAssertEqual(1, simulator.downloaders.count)
194 | XCTAssertEqual("http://site.com/auk.jpg", simulator.downloaders.first!.url)
195 |
196 | view.prepareForReuse()
197 |
198 | XCTAssert(simulator.downloaders.first!.cancelled)
199 | XCTAssert(imageView.moa.url == nil)
200 | XCTAssertNil(view.remoteImage)
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/AukTests/AukPageIndicatorContainerTests.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import XCTest
3 | @testable import Auk
4 |
5 | class AukPageIndicatorTests: XCTestCase {
6 |
7 | var settings: AukSettings!
8 | var container: AukPageIndicatorContainer!
9 | var scrollView: UIScrollView!
10 |
11 | override func setUp() {
12 | super.setUp()
13 |
14 | settings = AukSettings()
15 | container = AukPageIndicatorContainer()
16 | scrollView = UIScrollView()
17 | }
18 |
19 | override func tearDown() {
20 | super.tearDown()
21 | }
22 |
23 | // MARK: - Setup
24 |
25 | func testSetup_stylePageContainer() {
26 | settings.pageControl.cornerRadius = 14
27 | settings.pageControl.backgroundColor = UIColor.purple
28 |
29 | container.setup(settings, scrollView: scrollView)
30 |
31 | XCTAssertEqual(14, container.layer.cornerRadius)
32 | XCTAssertEqual(UIColor.purple, container.backgroundColor!)
33 | XCTAssert(container.isHidden)
34 | }
35 |
36 | func testSetup_layoutContainerView() {
37 | // Layout scroll view
38 | // ---------------
39 |
40 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
41 | scrollView.translatesAutoresizingMaskIntoConstraints = false
42 | superview.addSubview(scrollView)
43 | superview.addSubview(container)
44 |
45 | iiAutolayoutConstraints.height(scrollView, value: 100)
46 | iiAutolayoutConstraints.width(scrollView, value: 100)
47 |
48 | iiAutolayoutConstraints.alignSameAttributes(scrollView, toItem: superview,
49 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.left, margin: 0)
50 |
51 | iiAutolayoutConstraints.alignSameAttributes(scrollView, toItem: superview,
52 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.top, margin: 10)
53 |
54 | settings.pageControl.marginToScrollViewBottom = 13
55 |
56 | // Setup page view container
57 | container.setup(settings, scrollView: scrollView)
58 |
59 | superview.layoutIfNeeded()
60 |
61 | // Check container layout
62 | // ---------------
63 |
64 | XCTAssertEqual(97, container.frame.origin.y + container.bounds.height)
65 | XCTAssertEqual(50, container.frame.midX)
66 | }
67 |
68 | func testSetup_stylePageControl() {
69 | // Layout scroll view
70 | // ---------------
71 |
72 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
73 | superview.addSubview(scrollView)
74 |
75 | settings.pageControl.pageIndicatorTintColor = UIColor.blue
76 | settings.pageControl.currentPageIndicatorTintColor = UIColor.red
77 |
78 | // Create the views
79 | container.setup(settings, scrollView: scrollView)
80 |
81 | let pageControl = container.subviews[0] as! UIPageControl
82 |
83 | // Verify page control layout
84 | // ---------------
85 |
86 | XCTAssertEqual(UIColor.blue, pageControl.pageIndicatorTintColor!)
87 | XCTAssertEqual(UIColor.red, pageControl.currentPageIndicatorTintColor!)
88 | }
89 |
90 | func testSetup_layoutPageControl() {
91 | // Layout scroll view
92 | // ---------------
93 |
94 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
95 | scrollView.translatesAutoresizingMaskIntoConstraints = false
96 | superview.addSubview(scrollView)
97 | superview.addSubview(container)
98 |
99 | iiAutolayoutConstraints.height(scrollView, value: 100)
100 | iiAutolayoutConstraints.width(scrollView, value: 100)
101 |
102 | iiAutolayoutConstraints.alignSameAttributes(scrollView, toItem: superview,
103 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.left, margin: 0)
104 |
105 | iiAutolayoutConstraints.alignSameAttributes(scrollView, toItem: superview,
106 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.top, margin: 10)
107 |
108 | settings.pageControl.innerPadding = CGSize(width: 13, height: 18)
109 |
110 |
111 | // Create the views
112 | container.setup(settings, scrollView: scrollView)
113 |
114 | superview.layoutIfNeeded()
115 | container.layoutIfNeeded()
116 |
117 | let pageControl = container.subviews[0] as! UIPageControl
118 |
119 | // Verify page control layout
120 | // ---------------
121 |
122 | XCTAssertEqual(13 * 2, container.bounds.width - pageControl.bounds.width)
123 | XCTAssertEqual(18 * 2, container.bounds.height - pageControl.bounds.height)
124 | }
125 |
126 | // MARK: - Update number of pages
127 |
128 | func testUpdateNumberOfPages() {
129 | container.setup(settings, scrollView: scrollView)
130 | container.updateNumberOfPages(3)
131 |
132 | let pageControl = container.subviews[0] as! UIPageControl
133 | XCTAssertEqual(3, pageControl.numberOfPages)
134 | }
135 |
136 | // MARK: - Update current page
137 |
138 | func testUpdateCurrentPage() {
139 | container.setup(settings, scrollView: scrollView)
140 | container.updateNumberOfPages(10)
141 | container.updateCurrentPage(7)
142 |
143 | let pageControl = container.subviews[0] as! UIPageControl
144 | XCTAssertEqual(7, pageControl.currentPage)
145 | }
146 |
147 | // MARK: - Show / hide
148 |
149 | func testHiddenForOnePage() {
150 | container.setup(settings, scrollView: scrollView)
151 | container.updateNumberOfPages(1)
152 | XCTAssert(container.isHidden)
153 | }
154 |
155 | func testVisibleForTwoPages() {
156 | container.setup(settings, scrollView: scrollView)
157 | container.updateNumberOfPages(2)
158 | XCTAssertFalse(container.isHidden)
159 | }
160 |
161 | func testHideWhenNoPages() {
162 | container.setup(settings, scrollView: scrollView)
163 | container.updateNumberOfPages(2)
164 | container.updateNumberOfPages(0)
165 |
166 | XCTAssert(container.isHidden)
167 | }
168 |
169 | // MARK: Tap container
170 |
171 | func testTapContainer() {
172 | var receivePageIndex: Int?
173 |
174 | container.didTapPageControlCallback = { pageIndex in
175 | receivePageIndex = pageIndex
176 | }
177 |
178 | container.setup(settings, scrollView: scrollView)
179 | container.pageControl?.numberOfPages = 1000
180 | container.pageControl?.currentPage = 312
181 | container.didTapPageControl(container.pageControl!)
182 |
183 | XCTAssertEqual(312, receivePageIndex!)
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/Demo/ViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Auk
3 | import moa
4 |
5 | class ViewController: UIViewController, UIScrollViewDelegate {
6 | @IBOutlet weak var scrollView: UIScrollView!
7 |
8 | var imageDescriptions = [String]()
9 | @IBOutlet weak var imageDescriptionLabel: UILabel!
10 |
11 | @IBOutlet weak var deleteButton: UIButton!
12 | @IBOutlet weak var leftButton: UIButton!
13 | @IBOutlet weak var rightButton: UIButton!
14 | @IBOutlet weak var autoScrollButton: UIButton!
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 |
19 | scrollView.delegate = self
20 | scrollView.auk.settings.placeholderImage = UIImage(named: "great_auk_placeholder.png")
21 | scrollView.auk.settings.errorImage = UIImage(named: "error_image.png")
22 |
23 | // Preload the next and previous images
24 | scrollView.auk.settings.preloadRemoteImagesAround = 1
25 |
26 | // Turn on the image logger. The download log will be visible in the Xcode console
27 | Moa.logger = MoaConsoleLogger
28 |
29 | showInitialImage()
30 | showCurrentImageDescription()
31 | }
32 |
33 | // Show the first image when the app starts
34 | private func showInitialImage() {
35 | if let image = UIImage(named: DemoConstants.initialImage.fileName) {
36 | scrollView.auk.show(image: image,
37 | accessibilityLabel: DemoConstants.initialImage.description)
38 |
39 | imageDescriptions.append(DemoConstants.initialImage.description)
40 | }
41 | }
42 |
43 | // Show local images
44 | @IBAction func onShowLocalTapped(_ sender: AnyObject) {
45 | scrollView.auk.stopAutoScroll()
46 | for localImage in DemoConstants.localImages {
47 | if let image = UIImage(named: localImage.fileName) {
48 | scrollView.auk.show(image: image, accessibilityLabel: localImage.description)
49 | imageDescriptions.append(localImage.description)
50 | }
51 | }
52 |
53 | showCurrentImageDescription()
54 | }
55 |
56 | // Show remote images
57 | @IBAction func onShowRemoteTapped(_ sender: AnyObject) {
58 | scrollView.auk.stopAutoScroll()
59 | for remoteImage in DemoConstants.remoteImages {
60 | let url = "\(DemoConstants.remoteImageBaseUrl)\(remoteImage.fileName)"
61 | scrollView.auk.show(url: url, accessibilityLabel: remoteImage.description)
62 |
63 | imageDescriptions.append(remoteImage.description)
64 | }
65 |
66 | showCurrentImageDescription()
67 | }
68 |
69 | // Scroll to the next image
70 | @IBAction func onShowRightButtonTapped(_ sender: AnyObject) {
71 | scrollView.auk.stopAutoScroll()
72 |
73 | if RightToLeft.isRightToLeft(view) {
74 | scrollView.auk.scrollToPreviousPage()
75 | } else {
76 | scrollView.auk.scrollToNextPage()
77 | }
78 | }
79 |
80 | // Scroll to the previous image
81 | @IBAction func onShowLeftButtonTapped(_ sender: AnyObject) {
82 | scrollView.auk.stopAutoScroll()
83 |
84 | if RightToLeft.isRightToLeft(view) {
85 | scrollView.auk.scrollToNextPage()
86 | } else {
87 | scrollView.auk.scrollToPreviousPage()
88 | }
89 | }
90 |
91 | // Remove all images
92 | @IBAction func onDeleteButtonTapped(_ sender: AnyObject) {
93 | scrollView.auk.stopAutoScroll()
94 | scrollView.auk.removeAll()
95 | imageDescriptions = []
96 | showCurrentImageDescription()
97 | }
98 |
99 | @IBAction func onDeleteCurrentButtonTapped(_ sender: AnyObject) {
100 | guard let indexToRemove = scrollView.auk.currentPageIndex else { return }
101 | scrollView.auk.stopAutoScroll()
102 |
103 | scrollView.auk.removeCurrentPage(animated: true)
104 |
105 | if imageDescriptions.count >= scrollView.auk.numberOfPages {
106 | imageDescriptions.remove(at: indexToRemove)
107 | }
108 |
109 | showCurrentImageDescription()
110 | }
111 |
112 | @IBAction func onAutoscrollTapped(_ sender: AnyObject) {
113 | scrollView.auk.startAutoScroll(delaySeconds: 2)
114 | }
115 |
116 | @IBAction func onScrollViewTapped(_ sender: AnyObject) {
117 | imageDescriptionLabel.text = "Tapped image #\(scrollView.auk.currentPageIndex ?? 42)"
118 | }
119 |
120 | // MARK: - Handle orientation change
121 |
122 | /// Animate scroll view on orientation change
123 | override func viewWillTransition(to size: CGSize,
124 | with coordinator: UIViewControllerTransitionCoordinator) {
125 |
126 | super.viewWillTransition(to: size, with: coordinator)
127 |
128 | guard let pageIndex = scrollView.auk.currentPageIndex else { return }
129 | let newScrollViewWidth = size.width // Assuming scroll view occupies 100% of the screen width
130 |
131 | coordinator.animate(alongsideTransition: { [weak self] _ in
132 | self?.scrollView.auk.scrollToPage(atIndex: pageIndex, pageWidth: newScrollViewWidth, animated: false)
133 | }, completion: nil)
134 | }
135 |
136 | /// Animate scroll view on orientation change
137 | /// Support iOS 7 and older
138 | override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation,
139 | duration: TimeInterval) {
140 |
141 | super.willRotate(to: toInterfaceOrientation, duration: duration)
142 |
143 | var screenWidth = UIScreen.main.bounds.height
144 | if toInterfaceOrientation.isPortrait {
145 | screenWidth = UIScreen.main.bounds.width
146 | }
147 |
148 | guard let pageIndex = scrollView.auk.currentPageIndex else { return }
149 | scrollView.auk.scrollToPage(atIndex: pageIndex, pageWidth: screenWidth, animated: false)
150 | }
151 |
152 | // MARK: - Image description
153 |
154 | private func showCurrentImageDescription() {
155 | if let description = currentImageDescription {
156 | imageDescriptionLabel.text = description
157 | } else {
158 | imageDescriptionLabel.text = nil
159 | }
160 | }
161 |
162 | private func changeCurrentImageDescription(_ description: String) {
163 | guard let currentPageIndex = scrollView.auk.currentPageIndex else { return }
164 |
165 | if currentPageIndex >= imageDescriptions.count {
166 | return
167 | }
168 |
169 | imageDescriptions[currentPageIndex] = description
170 | showCurrentImageDescription()
171 | }
172 |
173 | private var currentImageDescription: String? {
174 | guard let currentPageIndex = scrollView.auk.currentPageIndex else { return nil }
175 |
176 | if currentPageIndex >= imageDescriptions.count {
177 | return nil
178 | }
179 |
180 | return imageDescriptions[currentPageIndex]
181 | }
182 |
183 | // MARK: - UIScrollViewDelegate
184 |
185 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
186 | showCurrentImageDescription()
187 | }
188 | }
189 |
190 |
--------------------------------------------------------------------------------
/AukTests/AukPageVisibilityTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import moa
3 | @testable import Auk
4 |
5 | class AukPageVisibilityTests: XCTestCase {
6 | var scrollView: UIScrollView!
7 | var settings: AukSettings!
8 |
9 | override func setUp() {
10 | super.setUp()
11 |
12 | scrollView = UIScrollView()
13 |
14 | // Set scroll view size
15 | let size = CGSize(width: 290, height: 200)
16 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
17 |
18 | settings = AukSettings()
19 | }
20 |
21 | override func tearDown() {
22 | super.tearDown()
23 |
24 | MoaSimulator.clear()
25 | }
26 |
27 | // MARK: - Is page visible
28 |
29 | func testIsVisible_firstPageVisible() {
30 | let aukPage1 = AukPage()
31 | scrollView.addSubview(aukPage1)
32 |
33 | let aukPage2 = AukPage()
34 | scrollView.addSubview(aukPage2)
35 |
36 | AukScrollViewContent.layout(scrollView)
37 | scrollView.layoutIfNeeded()
38 |
39 | XCTAssert(AukPageVisibility.isVisible(scrollView, page: aukPage1))
40 | XCTAssertFalse(AukPageVisibility.isVisible(scrollView, page: aukPage2))
41 | }
42 |
43 | func testIsVisible_bothPagesAreVisible() {
44 | let aukPage1 = AukPage()
45 | scrollView.addSubview(aukPage1)
46 |
47 | let aukPage2 = AukPage()
48 | scrollView.addSubview(aukPage2)
49 |
50 | AukScrollViewContent.layout(scrollView)
51 | scrollView.layoutIfNeeded()
52 |
53 | scrollView.contentOffset.x = 289
54 |
55 | XCTAssert(AukPageVisibility.isVisible(scrollView, page: aukPage1))
56 | XCTAssert(AukPageVisibility.isVisible(scrollView, page: aukPage2))
57 | }
58 |
59 | func testIsPageVisible_lastPageVisible() {
60 | let aukPage1 = AukPage()
61 | scrollView.addSubview(aukPage1)
62 |
63 | let aukPage2 = AukPage()
64 | scrollView.addSubview(aukPage2)
65 |
66 | AukScrollViewContent.layout(scrollView)
67 | scrollView.layoutIfNeeded()
68 |
69 | scrollView.contentOffset.x = 290
70 |
71 | XCTAssertFalse(AukPageVisibility.isVisible(scrollView, page: aukPage1))
72 | XCTAssert(AukPageVisibility.isVisible(scrollView, page: aukPage2))
73 | }
74 |
75 | // MARK: - tell pages about their visiblity
76 |
77 | func testTellPagesAboutTheirVisibility_theLastPageDownloadsTheImage() {
78 | let simulate = MoaSimulator.simulate("site.com")
79 |
80 | // Show first page with remote image
81 | let aukPage1 = AukPage()
82 | scrollView.addSubview(aukPage1)
83 | aukPage1.show(url: "http://site.com/image_one.jpg", settings: settings)
84 |
85 | // Show second page with remote image
86 | let aukPage2 = AukPage()
87 | scrollView.addSubview(aukPage2)
88 | aukPage2.show(url: "http://site.com/image_two.jpg", settings: settings)
89 |
90 | AukScrollViewContent.layout(scrollView)
91 | scrollView.layoutIfNeeded()
92 |
93 | // The second page is visible
94 | scrollView.contentOffset.x = 290
95 |
96 | // This will tell the second page that it is visible and it will start the download
97 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
98 | settings: settings,
99 | currentPageIndex: 1)
100 |
101 | XCTAssertEqual(1, simulate.downloaders.count)
102 | XCTAssertEqual("http://site.com/image_two.jpg", simulate.downloaders.first!.url)
103 | XCTAssertFalse(simulate.downloaders.first!.cancelled)
104 | }
105 |
106 | func testTellPagesAboutTheirVisibility_startAndCancelImageDownloads() {
107 | let simulate = MoaSimulator.simulate("site.com")
108 |
109 | // Show first page with remote image
110 | let aukPage1 = AukPage()
111 | scrollView.addSubview(aukPage1)
112 | aukPage1.show(url: "http://site.com/image_one.jpg", settings: settings)
113 |
114 | aukPage1.remoteImage?.downloadImage(settings)
115 |
116 | // Show second page with remote image
117 | let aukPage2 = AukPage()
118 | scrollView.addSubview(aukPage2)
119 | aukPage2.show(url: "http://site.com/image_two.jpg", settings: settings)
120 |
121 | // Show third page with remote image
122 | let aukPage3 = AukPage()
123 | scrollView.addSubview(aukPage3)
124 | aukPage3.show(url: "http://site.com/image_three.jpg", settings: settings)
125 |
126 | AukScrollViewContent.layout(scrollView)
127 | scrollView.layoutIfNeeded()
128 |
129 | // 1. Make the second page visible
130 | // ---------------------
131 |
132 | scrollView.contentOffset.x = 290
133 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
134 | settings: settings,
135 | currentPageIndex: 1)
136 |
137 | XCTAssertEqual(2, simulate.downloaders.count)
138 |
139 | // Do not cancel the first image download just yet because it is still very close
140 | XCTAssertFalse(simulate.downloaders.first!.cancelled)
141 |
142 | // 2. Scroll a little bit firther to cancel first image download and start the third image download
143 | // ---------------------
144 |
145 | scrollView.contentOffset.x = 350
146 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
147 | settings: settings,
148 | currentPageIndex: 1)
149 |
150 | // Download of third image is started
151 | XCTAssertEqual(3, simulate.downloaders.count)
152 | XCTAssertEqual("http://site.com/image_three.jpg", simulate.downloaders.last!.url)
153 | XCTAssertFalse(simulate.downloaders.last!.cancelled)
154 |
155 | // Now the download of first image is cancelled because it is scrolled way out of view
156 | XCTAssert(simulate.downloaders.first!.cancelled)
157 |
158 | // 3. Now scroll back to the second page
159 | // ---------------------
160 |
161 | scrollView.contentOffset.x = 290
162 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
163 | settings: settings,
164 | currentPageIndex: 1)
165 |
166 | XCTAssertEqual(3, simulate.downloaders.count)
167 |
168 | // Third image download is not cancelled yet because it is still close (even though it is not visible)
169 | XCTAssertFalse(simulate.downloaders.last!.cancelled)
170 |
171 | // 4. Finally, scroll more towards the start to cancel download of third image
172 | // ---------------------
173 |
174 | scrollView.contentOffset.x = 220
175 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
176 | settings: settings,
177 | currentPageIndex: 1)
178 |
179 | XCTAssertEqual(4, simulate.downloaders.count)
180 | let thirdImageDownloader = simulate.downloaders[2]
181 | XCTAssertEqual("http://site.com/image_three.jpg", thirdImageDownloader.url)
182 |
183 | // Third image download is cancelled as it is far away now
184 | XCTAssert(thirdImageDownloader.cancelled)
185 | }
186 | }
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceStartAutoScrollTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import UIKit
3 | @testable import Auk
4 |
5 | class AukInterfaceStartAutoScrollTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | var auk: Auk!
9 |
10 | override func setUp() {
11 | super.setUp()
12 |
13 | scrollView = UIScrollView()
14 |
15 | // Set scroll view size
16 | let size = CGSize(width: 120, height: 90)
17 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
18 |
19 | auk = Auk(scrollView: scrollView)
20 | }
21 |
22 | override func tearDown() {
23 | super.tearDown()
24 | auk.stopAutoScroll()
25 | }
26 |
27 | // MARK: - Start auto scroll
28 |
29 | func testStartAutoScroll_toPage1() {
30 | let image = createImage96px()
31 | auk.show(image: image)
32 | auk.show(image: image)
33 | auk.show(image: image)
34 |
35 | auk.startAutoScroll(delaySeconds: 0.2)
36 |
37 | let expectation = self.expectation(description: "scroll")
38 | iiQ.runAfterDelay(0.3) { expectation.fulfill() }
39 | waitForExpectations(timeout: 1) { _ in }
40 | XCTAssertEqual(1, auk.currentPageIndex)
41 | }
42 |
43 | func testStartAutoScrollMultipleTimes_onlyScrollsOnce() {
44 | let image = createImage96px()
45 | auk.show(image: image)
46 | auk.show(image: image)
47 | auk.show(image: image)
48 | auk.show(image: image)
49 | auk.show(image: image)
50 |
51 | auk.startAutoScroll(delaySeconds: 0.2)
52 | auk.startAutoScroll(delaySeconds: 0.2)
53 | auk.startAutoScroll(delaySeconds: 0.2)
54 |
55 | let expectation = self.expectation(description: "scroll")
56 | iiQ.runAfterDelay(0.3) { expectation.fulfill() }
57 | waitForExpectations(timeout: 1) { _ in }
58 | XCTAssertEqual(1, auk.currentPageIndex)
59 | }
60 |
61 | func testStartAutoScroll_toPage2() {
62 | let image = createImage96px()
63 | auk.show(image: image)
64 | auk.show(image: image)
65 | auk.show(image: image)
66 |
67 | auk.startAutoScroll(delaySeconds: 0.2)
68 |
69 | let expectation = self.expectation(description: "scroll")
70 | iiQ.runAfterDelay(0.5) { expectation.fulfill() }
71 | waitForExpectations(timeout: 1) { _ in }
72 | XCTAssertEqual(2, auk.currentPageIndex)
73 | }
74 |
75 | func testStartAutoScroll_cycleToPag0() {
76 | let image = createImage96px()
77 | auk.show(image: image)
78 | auk.show(image: image)
79 | auk.show(image: image)
80 |
81 | auk.startAutoScroll(delaySeconds: 0.2)
82 |
83 | let expectation = self.expectation(description: "scroll")
84 | iiQ.runAfterDelay(0.7) { expectation.fulfill() }
85 | waitForExpectations(timeout: 2) { _ in }
86 | XCTAssertEqual(0, auk.currentPageIndex)
87 | }
88 |
89 | // MARK: - With parameters, forward
90 |
91 | func testStartAutoScroll_withParameters_forward_toPage1() {
92 | let image = createImage96px()
93 | auk.show(image: image)
94 | auk.show(image: image)
95 | auk.show(image: image)
96 |
97 | auk.startAutoScroll(delaySeconds: 0.2, forward: true, cycle: true, animated: false)
98 |
99 | let expectation = self.expectation(description: "scroll")
100 | iiQ.runAfterDelay(0.3) { expectation.fulfill() }
101 | waitForExpectations(timeout: 1) { _ in }
102 | XCTAssertEqual(1, auk.currentPageIndex)
103 | }
104 |
105 | func testStartAutoScroll_withParameters_backwards_toPage2() {
106 | let image = createImage96px()
107 | auk.show(image: image)
108 | auk.show(image: image)
109 | auk.show(image: image)
110 |
111 | auk.startAutoScroll(delaySeconds: 0.2, forward: false, cycle: true, animated: false)
112 |
113 | let expectation = self.expectation(description: "scroll")
114 | iiQ.runAfterDelay(0.25) { expectation.fulfill() }
115 | waitForExpectations(timeout: 1) { _ in }
116 | XCTAssertEqual(2, auk.currentPageIndex)
117 | }
118 |
119 | // MARK: - With parameters, forward, cycle
120 |
121 | func testStartAutoScroll_withParameters_forward_cycle_toPage1() {
122 | let image = createImage96px()
123 | auk.show(image: image)
124 | auk.show(image: image)
125 | auk.show(image: image)
126 |
127 | auk.startAutoScroll(delaySeconds: 0.2, forward: true, cycle: true, animated: false)
128 |
129 | let expectation = self.expectation(description: "scroll")
130 | iiQ.runAfterDelay(0.7) { expectation.fulfill() }
131 | waitForExpectations(timeout: 2) { _ in }
132 | XCTAssertEqual(0, auk.currentPageIndex)
133 | }
134 |
135 | func testStartAutoScroll_withParameters_forward_noCycle_toPage1() {
136 | let image = createImage96px()
137 | auk.show(image: image)
138 | auk.show(image: image)
139 | auk.show(image: image)
140 |
141 | auk.startAutoScroll(delaySeconds: 0.2, forward: true, cycle: false, animated: false)
142 |
143 | let expectation = self.expectation(description: "scroll")
144 | iiQ.runAfterDelay(0.5) { expectation.fulfill() }
145 | waitForExpectations(timeout: 1) { _ in }
146 | XCTAssertEqual(2, auk.currentPageIndex)
147 | }
148 |
149 | // MARK: - With parameters, backwards, cycle
150 |
151 | func testStartAutoScroll_withParameters_backwards_cycle_toPage1() {
152 | let image = createImage96px()
153 | auk.show(image: image)
154 | auk.show(image: image)
155 | auk.show(image: image)
156 |
157 | auk.startAutoScroll(delaySeconds: 0.2, forward: false, cycle: true, animated: false)
158 |
159 | let expectation = self.expectation(description: "scroll")
160 | iiQ.runAfterDelay(0.3) { expectation.fulfill() }
161 | waitForExpectations(timeout: 1) { _ in }
162 | XCTAssertEqual(2, auk.currentPageIndex)
163 | }
164 |
165 | func testStartAutoScroll_withParameters_backwards_noCycle_toPage1() {
166 | let image = createImage96px()
167 | auk.show(image: image)
168 | auk.show(image: image)
169 | auk.show(image: image)
170 |
171 | auk.startAutoScroll(delaySeconds: 0.2, forward: false, cycle: false, animated: false)
172 |
173 | let expectation = self.expectation(description: "scroll")
174 | iiQ.runAfterDelay(0.5) { expectation.fulfill() }
175 | waitForExpectations(timeout: 1) { _ in }
176 | XCTAssertEqual(0, auk.currentPageIndex)
177 | }
178 |
179 | // MARK: - Stop autoscroll
180 |
181 | func testStopAutoScroll() {
182 | let image = createImage96px()
183 | auk.show(image: image)
184 | auk.show(image: image)
185 | auk.show(image: image)
186 |
187 | auk.startAutoScroll(delaySeconds: 0.2)
188 | auk.stopAutoScroll()
189 |
190 | let expectation = self.expectation(description: "scroll")
191 | iiQ.runAfterDelay(0.3) { expectation.fulfill() }
192 | waitForExpectations(timeout: 1) { _ in }
193 | XCTAssertEqual(0, auk.currentPageIndex)
194 | }
195 |
196 | // MARK: - Cancel autoscroll when it is scrolled by user.
197 |
198 | func testStopAutoScrollWhenScrolledByUser() {
199 | let image = createImage96px()
200 | auk.show(image: image)
201 | auk.show(image: image)
202 | auk.show(image: image)
203 |
204 | auk.startAutoScroll(delaySeconds: 0.2)
205 |
206 | auk.scrollViewDelegate.scrollViewWillBeginDragging(scrollView)
207 |
208 | let expectation = self.expectation(description: "scroll")
209 | iiQ.runAfterDelay(0.3) { expectation.fulfill() }
210 | waitForExpectations(timeout: 1) { _ in }
211 | XCTAssertEqual(0, auk.currentPageIndex)
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/AukTests/Interface/AukInterfaceUpdateRemoteImageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import moa
3 | @testable import Auk
4 |
5 | class AukInterfaceUpdateRemoteImageTests: XCTestCase {
6 | var scrollView: UIScrollView!
7 | var auk: Auk!
8 |
9 | override func setUp() {
10 | super.setUp()
11 |
12 | scrollView = UIScrollView()
13 |
14 | // Set scroll view size
15 | let size = CGSize(width: 120, height: 90)
16 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
17 |
18 | auk = Auk(scrollView: scrollView)
19 | }
20 |
21 | override func tearDown() {
22 | super.tearDown()
23 |
24 | MoaSimulator.clear()
25 | }
26 |
27 | func testUpdateRemoteImage_overLocalImage() {
28 | let image = createImage96px()
29 | auk.show(image: image)
30 |
31 | let simulator = MoaSimulator.simulate("auk.png")
32 |
33 | auk.updatePage(atIndex: 0, url: "http://site.com/auk.png")
34 |
35 | XCTAssertEqual(1, simulator.downloaders.count)
36 | XCTAssertEqual("http://site.com/auk.png", simulator.downloaders.first!.url)
37 |
38 | let image67px = createImage67px()
39 | simulator.respondWithImage(image67px)
40 |
41 | XCTAssertEqual(1, aukPages(scrollView).count)
42 |
43 | // Show a placeholder image and a remote image on top
44 | XCTAssertEqual(2, numberOfImagesOnPage(scrollView, pageIndex: 0))
45 |
46 | // Uses previous image as placeholder
47 | XCTAssertEqual(96, firstAukImageWidth(scrollView, pageIndex: 0))
48 |
49 | // Shows new image on top
50 | XCTAssertEqual(67, secondAukImage(scrollView, pageIndex: 0)!.size.width)
51 | }
52 |
53 | func testUpdateRemoteImage_updateOnlyGivenSingePage() {
54 | // Show two images
55 | let image96px = createImage96px()
56 | auk.show(image: image96px)
57 |
58 | let image35px = createImage35px()
59 | auk.show(image: image35px)
60 |
61 | let simulator = MoaSimulator.simulate("auk.png")
62 |
63 | // Update image on first page with remote image
64 | auk.updatePage(atIndex: 0, url: "http://site.com/auk.png")
65 |
66 | XCTAssertEqual(1, simulator.downloaders.count)
67 | XCTAssertEqual("http://site.com/auk.png", simulator.downloaders.first!.url)
68 |
69 | let image67px = createImage67px()
70 | simulator.respondWithImage(image67px)
71 |
72 | XCTAssertEqual(2, aukPages(scrollView).count)
73 |
74 | // First page
75 | // -------------
76 |
77 | // Show a placeholder image and a remote image on top
78 | XCTAssertEqual(2, numberOfImagesOnPage(scrollView, pageIndex: 0))
79 |
80 | // Uses previous image as placeholder
81 | XCTAssertEqual(96, firstAukImageWidth(scrollView, pageIndex: 0))
82 |
83 | // Shows new image on top
84 | XCTAssertEqual(67, secondAukImage(scrollView, pageIndex: 0)!.size.width)
85 |
86 | // Second page
87 | // -------------
88 |
89 | // Show a single image without placeholder
90 | XCTAssertEqual(1, numberOfImagesOnPage(scrollView, pageIndex: 1))
91 |
92 | // Shows image
93 | XCTAssertEqual(35, firstAukImageWidth(scrollView, pageIndex: 1))
94 | }
95 |
96 | func testUpdateRemoteImage_overRemoteImage() {
97 | let simulator = MoaSimulator.simulate(".png")
98 | auk.show(url: "http://site.com/auk.png")
99 |
100 | auk.updatePage(atIndex: 0, url: "http://site.com/moa.png")
101 |
102 | XCTAssertEqual(2, simulator.downloaders.count)
103 |
104 | // Cancels the previous download
105 | XCTAssertEqual("http://site.com/auk.png", simulator.downloaders.first!.url)
106 | XCTAssert(simulator.downloaders.first!.cancelled)
107 |
108 | // Downloads the new one
109 | XCTAssertEqual("http://site.com/moa.png", simulator.downloaders.last!.url)
110 |
111 | let image67px = createImage67px()
112 | simulator.respondWithImage(image67px)
113 |
114 | XCTAssertEqual(1, aukPages(scrollView).count)
115 |
116 | // Show a remote image without placeholder image
117 | XCTAssertEqual(1, numberOfImagesOnPage(scrollView, pageIndex: 0))
118 |
119 | // Loads image
120 | XCTAssertEqual(67, firstAukImageWidth(scrollView, pageIndex: 0))
121 | }
122 |
123 | func testUpdateRemoteImage_withPlaceholderImage() {
124 | let simulator = MoaSimulator.simulate(".png")
125 |
126 | let image67px = createImage67px()
127 | auk.settings.placeholderImage = image67px
128 |
129 | auk.show(url: "http://site.com/auk.png")
130 |
131 | auk.updatePage(atIndex: 0, url: "http://site.com/moa.png")
132 |
133 | let image96px = createImage96px()
134 | simulator.respondWithImage(image96px)
135 |
136 | // Show a placeholder image and a remote image on top
137 | XCTAssertEqual(2, numberOfImagesOnPage(scrollView, pageIndex: 0))
138 |
139 | // Shows placeholder image
140 | XCTAssertEqual(67, firstAukImageWidth(scrollView, pageIndex: 0))
141 |
142 | // Shows remote image
143 | XCTAssertEqual(96, secondAukImage(scrollView, pageIndex: 0)!.size.width)
144 | }
145 |
146 | func testUpdateRemoteImage_showUpdatedRemoteImageWhenScrolled() {
147 | let simulator = MoaSimulator.simulate("site.com")
148 |
149 | // Add two local images
150 | let image96px = createImage96px()
151 | auk.show(image: image96px)
152 |
153 | let image67px = createImage67px()
154 | auk.show(image: image67px)
155 |
156 | // Update the second image with remote image
157 | auk.updatePage(atIndex: 1, url: "http://site.com/moa.png")
158 |
159 | // The updated image download has not started yet because the page is not visible
160 | XCTAssertEqual(0, simulator.downloaders.count)
161 |
162 | // Scroll to make the second page visible and start download
163 | scrollView.contentOffset.x = 10
164 | scrollView.delegate?.scrollViewDidScroll?(scrollView)
165 |
166 | // The remote image is requested
167 | XCTAssertEqual(1, simulator.downloaders.count)
168 | XCTAssertEqual("http://site.com/moa.png", simulator.downloaders.last!.url)
169 |
170 | // Verify that remote image is loaded to a second page
171 | // --------------
172 |
173 | let image35px = createImage35px()
174 | simulator.respondWithImage(image35px)
175 |
176 | // Show a placeholder image and a remote image on top
177 | XCTAssertEqual(2, numberOfImagesOnPage(scrollView, pageIndex: 1))
178 |
179 | // Shows placeholder image
180 | XCTAssertEqual(67, firstAukImageWidth(scrollView, pageIndex: 1))
181 |
182 | // Shows remote image
183 | XCTAssertEqual(35, secondAukImageWidth(scrollView, pageIndex: 1))
184 | }
185 |
186 | func testUpdateRemoteImage_indexLargerThanExist() {
187 | let simulator = MoaSimulator.simulate(".png")
188 |
189 | let image = createImage96px()
190 | auk.show(image: image)
191 |
192 | auk.updatePage(atIndex: 1, url: "http://site.com/moa.png")
193 |
194 | XCTAssertEqual(0, simulator.downloaders.count)
195 | XCTAssertEqual(96, firstAukImageWidth(scrollView, pageIndex: 0))
196 | }
197 |
198 | func testUpdateRemoteImage_indexNegative() {
199 | let simulator = MoaSimulator.simulate(".png")
200 |
201 | let image = createImage96px()
202 | auk.show(image: image)
203 |
204 | auk.updatePage(atIndex: -1, url: "http://site.com/moa.png")
205 |
206 | XCTAssertEqual(0, simulator.downloaders.count)
207 | XCTAssertEqual(96, firstAukImageWidth(scrollView, pageIndex: 0))
208 | }
209 |
210 | func testUpdateRemoteImage_noImagesToUpdate() {
211 | let simulator = MoaSimulator.simulate(".png")
212 |
213 | auk.updatePage(atIndex: -1, url: "http://site.com/moa.png")
214 |
215 | XCTAssertEqual(0, simulator.downloaders.count)
216 | XCTAssertEqual(0, aukPages(scrollView).count)
217 | }
218 |
219 | // MARK: - Accessibility
220 |
221 | func testUpdateAccessiblePageView_withLabel() {
222 | let _ = MoaSimulator.simulate(".png")
223 | let image = createImage96px()
224 | auk.show(image: image, accessibilityLabel: "Penguin")
225 |
226 | auk.updatePage(atIndex: 0, url: "http://site.com/auk.png",
227 | accessibilityLabel: "White knight riding a wooden horse on wheels.")
228 |
229 | let page = aukPage(scrollView, pageIndex: 0)!
230 |
231 | XCTAssert(page.isAccessibilityElement)
232 | XCTAssertEqual(page.accessibilityTraits, UIAccessibilityTraits.image)
233 | XCTAssertEqual("White knight riding a wooden horse on wheels.", page.accessibilityLabel!)
234 | }
235 |
236 | func testUpdateAccessiblePageView_removeExistingLabel() {
237 | let _ = MoaSimulator.simulate(".png")
238 | let image = createImage96px()
239 | auk.show(image: image, accessibilityLabel: "Penguin")
240 |
241 | auk.updatePage(atIndex: 0, url: "http://site.com/auk.png")
242 |
243 | let page = aukPage(scrollView, pageIndex: 0)!
244 |
245 | XCTAssert(page.isAccessibilityElement)
246 | XCTAssertEqual(page.accessibilityTraits, UIAccessibilityTraits.image)
247 | XCTAssert(page.accessibilityLabel == nil)
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/AukTests/AukPageVisibility_preloadImageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import moa
3 | @testable import Auk
4 |
5 | class AukPageVisibility_preloadImageTests: XCTestCase {
6 | var scrollView: UIScrollView!
7 | var settings: AukSettings!
8 |
9 | override func setUp() {
10 | super.setUp()
11 |
12 | scrollView = UIScrollView()
13 |
14 | // Set scroll view size
15 | let size = CGSize(width: 290, height: 200)
16 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
17 |
18 | settings = AukSettings()
19 | }
20 |
21 | override func tearDown() {
22 | super.tearDown()
23 |
24 | MoaSimulator.clear()
25 | settings.preloadRemoteImagesAround = 0
26 | }
27 |
28 | func testTellPagesAboutTheirVisibility_doNotPreloadImages() {
29 | settings.preloadRemoteImagesAround = 0
30 | let simulate = MoaSimulator.simulate("site.com")
31 |
32 | // Show first page with remote image
33 | let aukPage1 = AukPage()
34 | scrollView.addSubview(aukPage1)
35 | aukPage1.show(url: "http://site.com/image_one.jpg", settings: settings)
36 |
37 | // Show second page with remote image
38 | let aukPage2 = AukPage()
39 | scrollView.addSubview(aukPage2)
40 | aukPage2.show(url: "http://site.com/image_two.jpg", settings: settings)
41 |
42 | AukScrollViewContent.layout(scrollView)
43 | scrollView.layoutIfNeeded()
44 |
45 | // The first page is visible
46 | scrollView.contentOffset.x = 0
47 |
48 | // This will tell the second page that it is visible and it will start the download
49 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
50 | settings: settings,
51 | currentPageIndex: 0)
52 |
53 | // Load only first page image
54 | XCTAssertEqual(1, simulate.downloaders.count)
55 |
56 | // Download first image
57 | XCTAssertEqual("http://site.com/image_one.jpg", simulate.downloaders.first!.url)
58 | XCTAssertFalse(simulate.downloaders.first!.cancelled)
59 | }
60 |
61 | func testTellPagesAboutTheirVisibility_preloadOneImage() {
62 | settings.preloadRemoteImagesAround = 1
63 | let simulate = MoaSimulator.simulate("site.com")
64 |
65 | // Show first page with remote image
66 | let aukPage1 = AukPage()
67 | scrollView.addSubview(aukPage1)
68 | aukPage1.show(url: "http://site.com/image_one.jpg", settings: settings)
69 |
70 | // Show second page with remote image
71 | let aukPage2 = AukPage()
72 | scrollView.addSubview(aukPage2)
73 | aukPage2.show(url: "http://site.com/image_two.jpg", settings: settings)
74 |
75 | // Show third page with remote image
76 | let aukPage3 = AukPage()
77 | scrollView.addSubview(aukPage3)
78 | aukPage3.show(url: "http://site.com/image_three.jpg", settings: settings)
79 |
80 | AukScrollViewContent.layout(scrollView)
81 | scrollView.layoutIfNeeded()
82 |
83 | // The first page is visible
84 | scrollView.contentOffset.x = 0
85 |
86 | // This will tell the second page that it is visible and it will start the download
87 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
88 | settings: settings,
89 | currentPageIndex: 0)
90 |
91 | // Load images for first two pages
92 | XCTAssertEqual(2, simulate.downloaders.count)
93 |
94 | // Download first image
95 | XCTAssertEqual("http://site.com/image_one.jpg", simulate.downloaders.first!.url)
96 | XCTAssertFalse(simulate.downloaders.first!.cancelled)
97 |
98 | // Download second image
99 | XCTAssertEqual("http://site.com/image_two.jpg", simulate.downloaders[1].url)
100 | XCTAssertFalse(simulate.downloaders[1].cancelled)
101 | }
102 |
103 | func testTellPagesAboutTheirVisibility_preloadOneImage_scrolledToSecond() {
104 | settings.preloadRemoteImagesAround = 1
105 | let simulate = MoaSimulator.simulate("site.com")
106 |
107 | // Show first page with remote image
108 | let aukPage1 = AukPage()
109 | scrollView.addSubview(aukPage1)
110 | aukPage1.show(url: "http://site.com/image_one.jpg", settings: settings)
111 |
112 | // Show second page with remote image
113 | let aukPage2 = AukPage()
114 | scrollView.addSubview(aukPage2)
115 | aukPage2.show(url: "http://site.com/image_two.jpg", settings: settings)
116 |
117 | // Show third page with remote image
118 | let aukPage3 = AukPage()
119 | scrollView.addSubview(aukPage3)
120 | aukPage3.show(url: "http://site.com/image_three.jpg", settings: settings)
121 |
122 | // Show third page with remote image
123 | let aukPage4 = AukPage()
124 | scrollView.addSubview(aukPage4)
125 | aukPage4.show(url: "http://site.com/image_four.jpg", settings: settings)
126 |
127 | // Show third page with remote image
128 | let aukPage5 = AukPage()
129 | scrollView.addSubview(aukPage5)
130 | aukPage5.show(url: "http://site.com/image_five.jpg", settings: settings)
131 |
132 | AukScrollViewContent.layout(scrollView)
133 | scrollView.layoutIfNeeded()
134 |
135 | // The second page is visible
136 | scrollView.contentOffset.x = 290
137 |
138 | // This will tell the second page that it is visible and it will start the download
139 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
140 | settings: settings,
141 | currentPageIndex: 1)
142 |
143 | // Load images for three pages: the current, the previous and the next one.
144 | XCTAssertEqual(3, simulate.downloaders.count)
145 |
146 | // Download first image
147 | XCTAssertEqual("http://site.com/image_one.jpg", simulate.downloaders.first!.url)
148 | XCTAssertFalse(simulate.downloaders.first!.cancelled)
149 |
150 | // Download second image
151 | XCTAssertEqual("http://site.com/image_two.jpg", simulate.downloaders[1].url)
152 | XCTAssertFalse(simulate.downloaders[1].cancelled)
153 |
154 | // Download third image
155 | XCTAssertEqual("http://site.com/image_three.jpg", simulate.downloaders[2].url)
156 | XCTAssertFalse(simulate.downloaders[2].cancelled)
157 |
158 | // Now scroll to third page to cancel the first image download
159 | // -------------------
160 |
161 | scrollView.contentOffset.x = 580
162 |
163 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
164 | settings: settings,
165 | currentPageIndex: 2)
166 |
167 | XCTAssertEqual(4, simulate.downloaders.count)
168 |
169 | // Cancel first image
170 | XCTAssertEqual("http://site.com/image_one.jpg", simulate.downloaders.first!.url)
171 | XCTAssertTrue(simulate.downloaders.first!.cancelled)
172 |
173 | // Download second image
174 | XCTAssertEqual("http://site.com/image_two.jpg", simulate.downloaders[1].url)
175 | XCTAssertFalse(simulate.downloaders[1].cancelled)
176 |
177 | // Download third image
178 | XCTAssertEqual("http://site.com/image_three.jpg", simulate.downloaders[2].url)
179 | XCTAssertFalse(simulate.downloaders[2].cancelled)
180 |
181 | // Download fourth image
182 | XCTAssertEqual("http://site.com/image_four.jpg", simulate.downloaders[3].url)
183 | XCTAssertFalse(simulate.downloaders[3].cancelled)
184 |
185 |
186 | // Now scroll back to first image.
187 | // This cancels the fourth image download
188 | // and starts downloading the first again.
189 | // -------------------
190 |
191 | scrollView.contentOffset.x = 0
192 |
193 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
194 | settings: settings,
195 | currentPageIndex: 0)
196 |
197 | XCTAssertEqual(5, simulate.downloaders.count)
198 |
199 | // Cancel first image
200 | XCTAssertEqual("http://site.com/image_one.jpg", simulate.downloaders.first!.url)
201 | XCTAssertTrue(simulate.downloaders.first!.cancelled)
202 |
203 | // Download second image
204 | XCTAssertEqual("http://site.com/image_two.jpg", simulate.downloaders[1].url)
205 | XCTAssertFalse(simulate.downloaders[1].cancelled)
206 |
207 | // Cancel third image
208 | XCTAssertEqual("http://site.com/image_three.jpg", simulate.downloaders[2].url)
209 | XCTAssertTrue(simulate.downloaders[2].cancelled)
210 |
211 | // Cancel fourth image
212 | XCTAssertEqual("http://site.com/image_four.jpg", simulate.downloaders[3].url)
213 | XCTAssertTrue(simulate.downloaders[3].cancelled)
214 |
215 | // Download the first image again
216 | XCTAssertEqual("http://site.com/image_one.jpg", simulate.downloaders[4].url)
217 | XCTAssertFalse(simulate.downloaders[4].cancelled)
218 | }
219 |
220 | func testTellPagesAboutTheirVisibility_preloadTwoImages() {
221 | settings.preloadRemoteImagesAround = 2
222 | let simulate = MoaSimulator.simulate("site.com")
223 |
224 | // Show first page with remote image
225 | let aukPage1 = AukPage()
226 | scrollView.addSubview(aukPage1)
227 | aukPage1.show(url: "http://site.com/image_one.jpg", settings: settings)
228 |
229 | // Show second page with remote image
230 | let aukPage2 = AukPage()
231 | scrollView.addSubview(aukPage2)
232 | aukPage2.show(url: "http://site.com/image_two.jpg", settings: settings)
233 |
234 | // Show third page with remote image
235 | let aukPage3 = AukPage()
236 | scrollView.addSubview(aukPage3)
237 | aukPage3.show(url: "http://site.com/image_three.jpg", settings: settings)
238 |
239 | // Show third page with remote image
240 | let aukPage4 = AukPage()
241 | scrollView.addSubview(aukPage4)
242 | aukPage4.show(url: "http://site.com/image_four.jpg", settings: settings)
243 |
244 | AukScrollViewContent.layout(scrollView)
245 | scrollView.layoutIfNeeded()
246 |
247 | // The first page is visible
248 | scrollView.contentOffset.x = 0
249 |
250 | // This will tell the second page that it is visible and it will start the download
251 | AukPageVisibility.tellPagesAboutTheirVisibility(scrollView,
252 | settings: settings,
253 | currentPageIndex: 0)
254 |
255 | // Load images for first three pages
256 | XCTAssertEqual(3, simulate.downloaders.count)
257 |
258 | // Download first image
259 | XCTAssertEqual("http://site.com/image_one.jpg", simulate.downloaders.first!.url)
260 | XCTAssertFalse(simulate.downloaders.first!.cancelled)
261 |
262 | // Download second image
263 | XCTAssertEqual("http://site.com/image_two.jpg", simulate.downloaders[1].url)
264 | XCTAssertFalse(simulate.downloaders[1].cancelled)
265 |
266 | // Download third image
267 | XCTAssertEqual("http://site.com/image_three.jpg", simulate.downloaders[2].url)
268 | XCTAssertFalse(simulate.downloaders[2].cancelled)
269 | }
270 | }
271 |
272 |
--------------------------------------------------------------------------------
/AukTests/AukTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import moa
3 | @testable import Auk
4 |
5 | class AukTests: XCTestCase {
6 |
7 | var scrollView: UIScrollView!
8 | var auk: Auk!
9 | var fakeAnimator: iiFakeAnimator!
10 |
11 | override func setUp() {
12 | super.setUp()
13 |
14 | scrollView = UIScrollView()
15 |
16 | // Set scroll view size
17 | let size = CGSize(width: 120, height: 90)
18 | scrollView.bounds = CGRect(origin: CGPoint(), size: size)
19 |
20 | auk = Auk(scrollView: scrollView)
21 |
22 | // Use fake animator
23 | fakeAnimator = iiFakeAnimator()
24 | iiAnimator.currentAnimator = fakeAnimator
25 | }
26 |
27 | override func tearDown() {
28 | super.tearDown()
29 |
30 | iiAnimator.currentAnimator = nil // Remove the fake animator
31 | }
32 |
33 | // MARK: - Setup
34 |
35 | func testSetup_style() {
36 | auk = Auk(scrollView: scrollView)
37 | auk.setup()
38 |
39 | XCTAssertFalse(scrollView.showsHorizontalScrollIndicator)
40 | XCTAssert(scrollView.isPagingEnabled)
41 | }
42 |
43 | // MARK: Page indicator
44 |
45 | func testSetup_createPageIndicator() {
46 | // Layout scroll view
47 | // ---------------
48 |
49 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
50 | scrollView.translatesAutoresizingMaskIntoConstraints = false
51 | superview.addSubview(scrollView)
52 |
53 | iiAutolayoutConstraints.height(scrollView, value: 100)
54 | iiAutolayoutConstraints.width(scrollView, value: 100)
55 |
56 | iiAutolayoutConstraints.alignSameAttributes(scrollView, toItem: superview,
57 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.left, margin: 0)
58 |
59 | iiAutolayoutConstraints.alignSameAttributes(scrollView, toItem: superview,
60 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.top, margin: 0)
61 |
62 | auk = Auk(scrollView: scrollView)
63 |
64 | // Setup the auk which will create the page view
65 | // ---------------
66 |
67 | auk.settings.pageControl.marginToScrollViewBottom = 11
68 | auk.setup()
69 |
70 | superview.layoutIfNeeded()
71 |
72 | // Check the page indicator layout
73 | // -----------
74 |
75 | XCTAssertEqual(89, auk.pageIndicatorContainer!.frame.maxY)
76 | }
77 |
78 | func testSetup_doNotCreatePageIndicator() {
79 | // Layout scroll view
80 | // ---------------
81 |
82 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
83 | scrollView.translatesAutoresizingMaskIntoConstraints = false
84 | superview.addSubview(scrollView)
85 |
86 | iiAutolayoutConstraints.height(scrollView, value: 100)
87 | iiAutolayoutConstraints.width(scrollView, value: 100)
88 |
89 | iiAutolayoutConstraints.alignSameAttributes(scrollView, toItem: superview,
90 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.left, margin: 0)
91 |
92 | iiAutolayoutConstraints.alignSameAttributes(scrollView, toItem: superview,
93 | constraintContainer: superview, attribute: NSLayoutConstraint.Attribute.top, margin: 0)
94 |
95 | auk = Auk(scrollView: scrollView)
96 |
97 | // Setup the auk which will create the page view
98 | // ---------------
99 |
100 | auk.settings.pageControl.visible = false
101 | auk.setup()
102 |
103 | superview.layoutIfNeeded()
104 |
105 | // Check the page indicator layout
106 | // -----------
107 |
108 | XCTAssert(auk.pageIndicatorContainer == nil)
109 | }
110 |
111 | func testSetup_createSinglePageIndicator() {
112 | // Layout scroll view
113 | // ---------------
114 |
115 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
116 | superview.addSubview(scrollView)
117 |
118 | auk = Auk(scrollView: scrollView)
119 |
120 | // Call setup multiple times
121 | // ---------------
122 |
123 | auk.setup()
124 | auk.setup()
125 | auk.setup()
126 |
127 | // Verify that only one page indicator container has been created
128 | // ---------------
129 |
130 | let indicators = superview.subviews.filter { $0 as? AukPageIndicatorContainer != nil }
131 | XCTAssertEqual(1, indicators.count)
132 | }
133 |
134 | // MARK: - Update page indicator
135 |
136 | func testPageIndicator_updateCurrentPage_updateNumberOfPages() {
137 | // Layout scroll view
138 | // ---------------
139 |
140 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
141 | superview.addSubview(scrollView)
142 |
143 | // Show 3 images
144 | // -------------
145 |
146 | let image = createImage96px()
147 | auk.show(image: image)
148 | auk.show(image: image)
149 | auk.show(image: image)
150 |
151 | // Verify page indicator is showing three pages
152 | // -------------
153 |
154 | XCTAssertEqual(3, auk.pageIndicatorContainer!.pageControl!.numberOfPages)
155 | }
156 |
157 | func testPageIndicator_updateCurrentPage() {
158 | // Layout scroll view
159 | // ---------------
160 |
161 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
162 | superview.addSubview(scrollView)
163 |
164 | // Show 3 images
165 | // -------------
166 |
167 | let image = createImage96px()
168 | auk.show(image: image)
169 | auk.show(image: image)
170 | auk.show(image: image)
171 |
172 | // Scroll to the first page
173 | // ------------
174 |
175 | scrollView.contentOffset.x = 0
176 | scrollView.delegate?.scrollViewDidScroll?(scrollView)
177 | XCTAssertEqual(0, auk.pageIndicatorContainer!.pageControl!.currentPage)
178 |
179 | // Scroll to the second page
180 | // -------------
181 |
182 | scrollView.contentOffset.x = 120
183 | scrollView.delegate?.scrollViewDidScroll?(scrollView)
184 | XCTAssertEqual(1, auk.pageIndicatorContainer!.pageControl!.currentPage)
185 | }
186 |
187 | // MARK: - Show / hide page indicator
188 |
189 | func testPageIndicator_showForTwoPages() {
190 | // Layout scroll view
191 | // ---------------
192 |
193 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
194 | superview.addSubview(scrollView)
195 |
196 | let image = createImage96px()
197 | auk.show(image: image)
198 | auk.show(image: image)
199 |
200 | XCTAssertFalse(auk.pageIndicatorContainer!.isHidden)
201 | }
202 |
203 | func testPageIndicator_hideWhenPagesRemoved() {
204 | // Layout scroll view
205 | // ---------------
206 |
207 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
208 | superview.addSubview(scrollView)
209 |
210 | let image = createImage96px()
211 | auk.show(image: image)
212 | auk.show(image: image)
213 | auk.removeAll()
214 |
215 | XCTAssert(auk.pageIndicatorContainer!.isHidden)
216 | }
217 |
218 | func testPageIndicator_scrollWhenPageIndicatorIsTapped() {
219 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
220 | superview.addSubview(scrollView)
221 |
222 | let image = createImage96px()
223 | auk.show(image: image)
224 | auk.show(image: image)
225 |
226 | auk.pageIndicatorContainer!.pageControl!.currentPage = 1
227 | auk.pageIndicatorContainer?.didTapPageControl(auk.pageIndicatorContainer!.pageControl!)
228 |
229 | XCTAssertEqual(120, scrollView.contentOffset.x)
230 | }
231 |
232 |
233 | // MARK: - updatePageIndicator
234 |
235 | func testUpdateTestIndicator() {
236 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
237 | superview.addSubview(scrollView)
238 |
239 | let aukView1 = AukPage()
240 | let aukView2 = AukPage()
241 |
242 | scrollView.addSubview(aukView1)
243 | scrollView.addSubview(aukView2)
244 |
245 | // Create page indicator
246 | // ---------------
247 |
248 | let pageIndicator = AukPageIndicatorContainer()
249 | auk.pageIndicatorContainer = pageIndicator
250 | superview.addSubview(pageIndicator)
251 | pageIndicator.setup(auk.settings, scrollView: scrollView)
252 |
253 | superview.layoutIfNeeded()
254 |
255 | // Update page indicator
256 | // -------------
257 |
258 | XCTAssertEqual(0, auk.pageIndicatorContainer!.pageControl!.numberOfPages)
259 | XCTAssertEqual(-1, auk.pageIndicatorContainer!.pageControl!.currentPage)
260 |
261 | auk.updatePageIndicator()
262 |
263 | XCTAssertEqual(2, auk.pageIndicatorContainer!.pageControl!.numberOfPages)
264 | XCTAssertEqual(0, auk.pageIndicatorContainer!.pageControl!.currentPage)
265 |
266 | scrollView.contentOffset = CGPoint(x: 200, y: 0)
267 | XCTAssertEqual(1, auk.pageIndicatorContainer!.pageControl!.currentPage)
268 | auk.updatePageIndicator()
269 | }
270 |
271 | // MARK: - removePage
272 |
273 | func testRemovePage() {
274 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
275 | superview.addSubview(scrollView)
276 |
277 | let aukView1 = AukPage()
278 | let aukView2 = AukPage()
279 |
280 | scrollView.addSubview(aukView1)
281 | scrollView.addSubview(aukView2)
282 |
283 | auk.createPageIndicator()
284 | superview.layoutIfNeeded()
285 |
286 | scrollView.contentOffset = CGPoint(x: 200, y: 0)
287 | auk.updatePageIndicator()
288 |
289 | XCTAssertEqual(2, auk.pageIndicatorContainer!.pageControl!.numberOfPages)
290 | XCTAssertEqual(1, auk.pageIndicatorContainer!.pageControl!.currentPage)
291 |
292 | // Remove page
293 | // -------------
294 |
295 | auk.removePage(page: aukView2, animated: false)
296 |
297 | // Page is removed
298 | XCTAssertNil(aukView2.superview)
299 |
300 | // Page indicator is updated
301 | XCTAssertEqual(1, auk.pageIndicatorContainer!.pageControl!.numberOfPages)
302 | XCTAssertEqual(0, auk.pageIndicatorContainer!.pageControl!.currentPage)
303 | }
304 |
305 | func testRemovePage_callCompletion() {
306 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
307 | superview.addSubview(scrollView)
308 |
309 | let aukView1 = AukPage()
310 | let aukView2 = AukPage()
311 |
312 | scrollView.addSubview(aukView1)
313 | scrollView.addSubview(aukView2)
314 |
315 | superview.layoutIfNeeded()
316 |
317 | // Remove page
318 | // -------------
319 |
320 | var didCallCompletion = false
321 |
322 | auk.removePage(page: aukView2, animated: false, completion: {
323 | didCallCompletion = true
324 | })
325 |
326 | XCTAssert(didCallCompletion)
327 | }
328 |
329 | func testRemovePage_notifyPagesAboutTheirVisibitliy() {
330 | let simulate = MoaSimulator.simulate("site.com")
331 |
332 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
333 | superview.addSubview(scrollView)
334 |
335 | auk.show(url: "http://site.com/one.jpg")
336 | auk.show(url: "http://site.com/two.jpg")
337 |
338 | // Dowload the first page initially
339 | XCTAssertEqual(1, simulate.downloaders.count)
340 | XCTAssertEqual("http://site.com/one.jpg", simulate.downloaders[0].url)
341 |
342 | // Remove first page
343 | // -------------
344 |
345 | let page = aukPage(scrollView, pageIndex: 0)!
346 | auk.removePage(page: page, animated: false)
347 |
348 | // Dowload the second page
349 | XCTAssertEqual(2, simulate.downloaders.count)
350 | XCTAssertEqual("http://site.com/two.jpg", simulate.downloaders[1].url)
351 | }
352 |
353 | func testRemovePage_animated() {
354 | let superview = UIView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 300, height: 300)))
355 | superview.addSubview(scrollView)
356 |
357 | let aukView1 = AukPage()
358 | let aukView2 = AukPage()
359 |
360 | scrollView.addSubview(aukView1)
361 | scrollView.addSubview(aukView2)
362 |
363 | auk.createPageIndicator()
364 | superview.layoutIfNeeded()
365 |
366 | scrollView.contentOffset = CGPoint(x: 200, y: 0)
367 | auk.updatePageIndicator()
368 |
369 | XCTAssertEqual(2, auk.pageIndicatorContainer!.pageControl!.numberOfPages)
370 | XCTAssertEqual(1, auk.pageIndicatorContainer!.pageControl!.currentPage)
371 |
372 | // Remove page
373 | // -------------
374 |
375 | auk.removePage(page: aukView2, animated: true)
376 |
377 | // Check the animation
378 | XCTAssertEqual(1, fakeAnimator.testParameters.count)
379 | XCTAssertEqual(0.3, fakeAnimator.testParameters[0].duration) // Default layout animation duration
380 | }
381 | }
382 |
--------------------------------------------------------------------------------