├── 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 | --------------------------------------------------------------------------------