├── .swift-version ├── package.json ├── Screenshots ├── example01.gif ├── example02.gif └── example03.gif ├── SKPhotoBrowserExample ├── Podfile ├── SKPhotoBrowserExample │ ├── SampleImages │ │ ├── image0.jpg │ │ ├── image1.jpg │ │ ├── image10.jpg │ │ ├── image12.jpg │ │ ├── image2.jpg │ │ ├── image3.jpg │ │ ├── image4.jpg │ │ ├── image5.jpg │ │ ├── image6.jpg │ │ ├── image7.jpg │ │ ├── image8.jpg │ │ ├── image9.jpg │ │ └── sample_gif.gif │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── FromWebViewController.swift │ ├── FromCameraRollViewController.swift │ └── FromLocalViewController.swift ├── SKPhotoBrowserExample.xcodeproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── SKPhotoBrowserExample.xcworkspace │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── contents.xcworkspacedata └── Podfile.lock ├── SKPhotoBrowser ├── SKPhotoBrowser.bundle │ └── images │ │ ├── btn_common_back_wh.png │ │ ├── btn_common_close_wh.png │ │ ├── btn_common_delete_wh.png │ │ ├── btn_common_back_wh@2x.png │ │ ├── btn_common_back_wh@3x.png │ │ ├── btn_common_close_wh@2x.png │ │ ├── btn_common_close_wh@3x.png │ │ ├── btn_common_forward_wh.png │ │ ├── btn_common_delete_wh@2x.png │ │ ├── btn_common_delete_wh@3x.png │ │ ├── btn_common_forward_wh@2x.png │ │ └── btn_common_forward_wh@3x.png ├── extensions │ ├── UIImage+BundledImage.swift │ ├── UIView+Radius.swift │ ├── UIApplication+UIWindow.swift │ ├── UIView+ScreenshotProtection.swift │ ├── ObjC │ │ ├── UIImage+animatedGIF.h │ │ └── UIImage+animatedGIF.m │ └── UIImage+Rotation.swift ├── SKIndicatorView.swift ├── SKPhotoBrowser.h ├── SKCacheable.swift ├── Info.plist ├── SKDetectingView.swift ├── SKMesurement.swift ├── SKDetectingImageView.swift ├── SKLocalPhoto.swift ├── SKToolbar.swift ├── SKCache.swift ├── SKCaptionView.swift ├── SKPhotoBrowserOptions.swift ├── SKPhotoBrowserDelegate.swift ├── SKButtons.swift ├── SKActionView.swift ├── SKPhoto.swift ├── SKPaginationView.swift ├── SKAnimator.swift ├── SKPagingScrollView.swift └── SKZoomingScrollView.swift ├── SKPhotoBrowser.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── SKPhotoBrowser.xcscheme ├── .swiftlint.yml ├── .github └── workflows │ └── swift_build.yml ├── SKPhotoBrowserTests ├── Info.plist ├── SKPhotoBrowserTests.swift └── SKCacheTests.swift ├── .gitignore ├── SKPhotoBrowser.podspec ├── .travis.yml ├── Package.swift ├── LICENSE ├── CHANGELOG.md ├── README.md └── yarn.lock /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "all-contributors-cli": "^6.20.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Screenshots/example01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/Screenshots/example01.gif -------------------------------------------------------------------------------- /Screenshots/example02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/Screenshots/example02.gif -------------------------------------------------------------------------------- /Screenshots/example03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/Screenshots/example03.gif -------------------------------------------------------------------------------- /SKPhotoBrowserExample/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | use_frameworks! 3 | 4 | target "SKPhotoBrowserExample" do 5 | pod 'SDWebImage' 6 | end 7 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_back_wh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_back_wh.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_close_wh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_close_wh.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_delete_wh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_delete_wh.png -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image0.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image1.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image10.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image12.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image2.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image3.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image4.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image5.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image6.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image7.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image8.jpg -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/image9.jpg -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_back_wh@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_back_wh@2x.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_back_wh@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_back_wh@3x.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_close_wh@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_close_wh@2x.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_close_wh@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_close_wh@3x.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_forward_wh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_forward_wh.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_delete_wh@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_delete_wh@2x.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_delete_wh@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_delete_wh@3x.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_forward_wh@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_forward_wh@2x.png -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_forward_wh@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowser/SKPhotoBrowser.bundle/images/btn_common_forward_wh@3x.png -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/sample_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-0000/SKPhotoBrowser/HEAD/SKPhotoBrowserExample/SKPhotoBrowserExample/SampleImages/sample_gif.gif -------------------------------------------------------------------------------- /SKPhotoBrowser.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SKPhotoBrowser.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - type_body_length 3 | - line_length 4 | - trailing_newline 5 | - trailing_whitespace 6 | - variable_name 7 | - type_name 8 | - todo 9 | - nesting 10 | 11 | excluded: 12 | - Docs 13 | - Frameworks 14 | - Pods 15 | 16 | variable_name_min_length: 1 17 | variable_name_max_length: 40 18 | function_body_length: 350 19 | file_length: 800 20 | type_body_length: 800 21 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SDWebImage (3.8.2): 3 | - SDWebImage/Core (= 3.8.2) 4 | - SDWebImage/Core (3.8.2) 5 | 6 | DEPENDENCIES: 7 | - SDWebImage 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - SDWebImage 12 | 13 | SPEC CHECKSUMS: 14 | SDWebImage: 098e97e6176540799c27e804c96653ee0833d13c 15 | 16 | PODFILE CHECKSUM: 48d639fa560d87286e5314591af8a3a89c6c887e 17 | 18 | COCOAPODS: 1.10.1 19 | -------------------------------------------------------------------------------- /.github/workflows/swift_build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Show Xcode version 18 | run: xcodebuild -version 19 | 20 | - name: Build 21 | run: xcodebuild -sdk iphonesimulator -configuration Debug build 22 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SKPhotoBrowserExample 4 | // 5 | // Created by suzuki_keishi on 2015/10/09. 6 | // Copyright © 2015 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /SKPhotoBrowser/extensions/UIImage+BundledImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+BundledImage.swift 3 | // 4 | // 5 | // Created by Aleksandr Solovev on 09.06.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImage { 11 | static func bundledImage(named imageName: String) -> UIImage { 12 | let imagePath = "SKPhotoBrowser.bundle/images/\(imageName)" 13 | #if SWIFT_PACKAGE 14 | return UIImage(named: imagePath, in: .module, compatibleWith: nil) ?? UIImage() 15 | #else 16 | let bundle = Bundle(for: SKPhotoBrowser.self) 17 | return UIImage(named: imagePath, in: bundle, compatibleWith: nil) ?? UIImage() 18 | #endif 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKIndicatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKIndicatorView.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by suzuki_keishi on 2015/10/09. 6 | // Copyright © 2015 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SKIndicatorView: UIActivityIndicatorView { 12 | required init(coder aDecoder: NSCoder) { 13 | super.init(coder: aDecoder) 14 | } 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | center = CGPoint(x: frame.width / 2, y: frame.height / 2) 19 | style = SKPhotoBrowserOptions.indicatorStyle 20 | color = SKPhotoBrowserOptions.indicatorColor 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowser.h: -------------------------------------------------------------------------------- 1 | // 2 | // SKPhotoBrowser.h 3 | // SKPhotoBrowser 4 | // 5 | // Created by 鈴木 啓司 on 2015/10/09. 6 | // Copyright © 2015年 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #if __has_include("UIImage+animatedGIF.h") 12 | #import "UIImage+animatedGIF.h" 13 | #endif 14 | 15 | //! Project version number for SKPhotoBrowser. 16 | FOUNDATION_EXPORT double SKPhotoBrowserVersionNumber; 17 | 18 | //! Project version string for SKPhotoBrowser. 19 | FOUNDATION_EXPORT const unsigned char SKPhotoBrowserVersionString[]; 20 | 21 | // In this header, you should import all the public headers of your framework using statements like #import 22 | 23 | 24 | -------------------------------------------------------------------------------- /SKPhotoBrowser/extensions/UIView+Radius.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Radius.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by K Rummler on 15/03/16. 6 | // Copyright © 2016 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | func addCornerRadiusAnimation(_ from: CGFloat, to: CGFloat, duration: CFTimeInterval) { 13 | let animation = CABasicAnimation(keyPath: "cornerRadius") 14 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) 15 | animation.fromValue = from 16 | animation.toValue = to 17 | animation.duration = duration 18 | self.layer.add(animation, forKey: "cornerRadius") 19 | self.layer.cornerRadius = to 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SKPhotoBrowser/extensions/UIApplication+UIWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+UIWindow.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by Josef Dolezal on 25/09/2017. 6 | // Copyright © 2017 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension UIApplication { 12 | var preferredApplicationWindow: UIWindow? { 13 | // Since delegate window is of type UIWindow??, we have to 14 | // unwrap it twice to be sure the window is not nil 15 | if let appWindow = UIApplication.shared.delegate?.window, let window = appWindow { 16 | return window 17 | } else if let window = UIApplication.shared.keyWindow { 18 | return window 19 | } 20 | 21 | return nil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKCacheable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKCacheable.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by Kevin Wolkober on 6/13/16. 6 | // Copyright © 2016 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit.UIImage 10 | 11 | public protocol SKCacheable {} 12 | public protocol SKImageCacheable: SKCacheable { 13 | func imageForKey(_ key: String) -> UIImage? 14 | func setImage(_ image: UIImage, forKey key: String) 15 | func removeImageForKey(_ key: String) 16 | func removeAllImages() 17 | } 18 | 19 | public protocol SKRequestResponseCacheable: SKCacheable { 20 | func cachedResponseForRequest(_ request: URLRequest) -> CachedURLResponse? 21 | func storeCachedResponse(_ cachedResponse: CachedURLResponse, forRequest request: URLRequest) 22 | } 23 | -------------------------------------------------------------------------------- /SKPhotoBrowserTests/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 | -------------------------------------------------------------------------------- /SKPhotoBrowser/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 | -------------------------------------------------------------------------------- /SKPhotoBrowser/extensions/UIView+ScreenshotProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+ScreenshotProtection.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by  JuyeonYu on 2023/08/18. 6 | // Copyright © 2023 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | func protectScreenshot() { 13 | DispatchQueue.main.async { 14 | let textField = UITextField() 15 | textField.isSecureTextEntry = true 16 | self.addSubview(textField) 17 | textField.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true 18 | textField.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true 19 | textField.layer.removeFromSuperlayer() 20 | self.layer.superlayer?.insertSublayer(textField.layer, at: 0) 21 | textField.layer.sublayers?.last?.addSublayer(self.layer) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | # mac 4 | .DS_Store 5 | 6 | ### Swift ### 7 | # Xcode 8 | # 9 | build/ 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | *.xccheckout 20 | *.moved-aside 21 | DerivedData 22 | *.hmap 23 | *.ipa 24 | *.xcuserstate 25 | 26 | # CocoaPods 27 | # 28 | # We recommend against adding the Pods directory to your .gitignore. However 29 | # you should judge for yourself, the pros and cons are mentioned at: 30 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 31 | Pods/ 32 | 33 | # Carthage 34 | # 35 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 36 | Carthage/Build 37 | 38 | # npm 39 | package-lock.json 40 | node_modules/ 41 | 42 | # all-contributors 43 | .all-contributorsrc 44 | -------------------------------------------------------------------------------- /SKPhotoBrowser.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "SKPhotoBrowser" 3 | s.version = "7.0.0" 4 | s.summary = "Simple PhotoBrowser/Viewer iwritten by pure swift. inspired by facebook, twitter photo browsers." 5 | s.homepage = "https://github.com/suzuki-0000/SKPhotoBrowser" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "suzuki_keishi" => "keishi.1983@gmail.com" } 8 | s.source = { :git => "https://github.com/suzuki-0000/SKPhotoBrowser.git", :tag => s.version } 9 | s.platform = :ios, "8.0" 10 | s.source_files = "SKPhotoBrowser/**/*.{h,m,swift}" 11 | s.resources = "SKPhotoBrowser/SKPhotoBrowser.bundle" 12 | s.requires_arc = true 13 | s.frameworks = "UIKit" 14 | s.pod_target_xcconfig = { 'SWIFT_VERSION' => '5.0' } 15 | s.swift_version = "5.4" 16 | s.swift_versions = ['4.0', '4.2', '5.0', '5.1', '5.2', '5.3', '5.4'] 17 | end 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode7.2 3 | branches: 4 | only: 5 | - master 6 | env: 7 | global: 8 | - LC_CTYPE=en_US.UTF-8 9 | - LANG=en_US.UTF-8 10 | - FRAMEWORK_NAME="CountdownLabel" 11 | matrix: 12 | - DESTINATION="OS=9.2,name=iPhone 6" SCHEME="CountdownLabelTests" SDK="iphonesimulator9.2" $ACTION="test" 13 | 14 | before_install: 15 | - brew update 16 | - brew install carthage || brew outdated carthage || brew upgrade carthage 17 | - carthage version 18 | 19 | install: 20 | - gem install xcpretty 21 | - carthage bootstrap --no-use-binaries --platform iOS 22 | 23 | script: 24 | - set -o pipefail 25 | - xcodebuild -version 26 | - xcodebuild -showsdks 27 | - xcodebuild 28 | -project "$FRAMEWORK_NAME.xcodeproj" 29 | -scheme "$SCHEME" 30 | -sdk "$SDK" 31 | -destination "$DESTINATION" 32 | -configuration Debug 33 | ONLY_ACTIVE_ARCH=NO 34 | GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES 35 | GCC_GENERATE_TEST_COVERAGE_FILES=YES 36 | "$ACTION" 37 | | xcpretty -c 38 | 39 | after_success: 40 | - bash <(curl -s https://codecov.io/bash) 41 | 42 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // 3 | // Package.swift 4 | // 5 | 6 | import PackageDescription 7 | 8 | let package = Package( 9 | name: "SKPhotoBrowser", 10 | platforms: [ 11 | .iOS(.v9) 12 | ], 13 | products: [ 14 | .library( 15 | name: "SKPhotoBrowser", 16 | targets: ["SKPhotoBrowser"]) 17 | ], 18 | targets: [ 19 | .target( 20 | name: "SKPhotoBrowser", 21 | dependencies: ["SKPhotoBrowserObjC"], 22 | path: "SKPhotoBrowser", 23 | exclude: ["Info.plist", 24 | "extensions/ObjC"], 25 | resources: [ 26 | .copy("SKPhotoBrowser.bundle") 27 | ]), 28 | .target( 29 | name: "SKPhotoBrowserObjC", 30 | path: "SKPhotoBrowser/extensions/ObjC", 31 | publicHeadersPath: "."), 32 | .testTarget( 33 | name: "SKPhotoBrowserTests", 34 | dependencies: ["SKPhotoBrowser"], 35 | path: "SKPhotoBrowserTests", 36 | exclude: ["Info.plist"] 37 | ) 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 suzuki_keishi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKDetectingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKDetectingView.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by suzuki_keishi on 2015/10/01. 6 | // Copyright © 2015 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc protocol SKDetectingViewDelegate { 12 | func handleSingleTap(_ view: UIView, touch: UITouch) 13 | func handleDoubleTap(_ view: UIView, touch: UITouch) 14 | } 15 | 16 | class SKDetectingView: UIView { 17 | weak var delegate: SKDetectingViewDelegate? 18 | 19 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 20 | super.touchesEnded(touches, with: event) 21 | defer { 22 | _ = next 23 | } 24 | 25 | guard let touch = touches.first else { 26 | return 27 | } 28 | switch touch.tapCount { 29 | case 1 : handleSingleTap(touch) 30 | case 2 : handleDoubleTap(touch) 31 | default: break 32 | } 33 | } 34 | 35 | func handleSingleTap(_ touch: UITouch) { 36 | delegate?.handleSingleTap(self, touch: touch) 37 | } 38 | 39 | func handleDoubleTap(_ touch: UITouch) { 40 | delegate?.handleDoubleTap(self, touch: touch) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKMesurement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKMesurement.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by 鈴木 啓司 on 2016/08/09. 6 | // Copyright © 2016年 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | struct SKMesurement { 13 | static let isPhone: Bool = UIDevice.current.userInterfaceIdiom == .phone 14 | static let isPad: Bool = UIDevice.current.userInterfaceIdiom == .pad 15 | static var statusBarH: CGFloat { 16 | return UIApplication.shared.statusBarFrame.height 17 | } 18 | static var screenHeight: CGFloat { 19 | return UIApplication.shared.preferredApplicationWindow?.bounds.height ?? UIScreen.main.bounds.height 20 | } 21 | static var screenWidth: CGFloat { 22 | return UIApplication.shared.preferredApplicationWindow?.bounds.width ?? UIScreen.main.bounds.width 23 | } 24 | static var screenScale: CGFloat { 25 | return UIScreen.main.scale 26 | } 27 | static var screenRatio: CGFloat { 28 | return screenWidth / screenHeight 29 | } 30 | static var isPhoneX: Bool { 31 | let iPhoneXHeights: [CGFloat] = [2436, 2688, 1792] 32 | if isPhone, iPhoneXHeights.contains(UIScreen.main.nativeBounds.height) { 33 | return true 34 | } 35 | return false 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SKPhotoBrowserTests/SKPhotoBrowserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKPhotoBrowserTests.swift 3 | // SKPhotoBrowserTests 4 | // 5 | // Created by Alexsander on 4/2/16. 6 | // Copyright © 2016 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SKPhotoBrowser 11 | 12 | class FakeSKPhotoBrowser: SKPhotoBrowser { 13 | override func setup () { 14 | } 15 | } 16 | 17 | class SKPhotoBrowserTests: XCTestCase { 18 | 19 | override func setUp() { 20 | super.setUp() 21 | // Put setup code here. This method is called before the invocation of each test method in the class. 22 | } 23 | 24 | override func tearDown() { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | super.tearDown() 27 | } 28 | 29 | func testSKPhotoArray() { 30 | var images = [SKPhoto]() 31 | let photo = SKPhoto.photoWithImage(UIImage())// add some UIImage 32 | images.append(photo) 33 | _ = FakeSKPhotoBrowser(photos: images) 34 | } 35 | 36 | func testSKLocalPhotoArray() { 37 | var images = [SKLocalPhoto]() 38 | let photo = SKLocalPhoto.photoWithImageURL("") 39 | images.append(photo) 40 | _ = FakeSKPhotoBrowser(photos: images) 41 | } 42 | 43 | func testPerformanceExample() { 44 | // This is an example of a performance test case. 45 | self.measure { 46 | // Put the code you want to measure the time of here. 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKDetectingImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKDetectingImageView.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by suzuki_keishi on 2015/10/01. 6 | // Copyright © 2015 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc protocol SKDetectingImageViewDelegate { 12 | func handleImageViewSingleTap(_ touchPoint: CGPoint) 13 | func handleImageViewDoubleTap(_ touchPoint: CGPoint) 14 | } 15 | 16 | class SKDetectingImageView: UIImageView { 17 | weak var delegate: SKDetectingImageViewDelegate? 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | setup() 22 | } 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | setup() 27 | } 28 | 29 | @objc func handleDoubleTap(_ recognizer: UITapGestureRecognizer) { 30 | delegate?.handleImageViewDoubleTap(recognizer.location(in: self)) 31 | } 32 | 33 | @objc func handleSingleTap(_ recognizer: UITapGestureRecognizer) { 34 | delegate?.handleImageViewSingleTap(recognizer.location(in: self)) 35 | } 36 | } 37 | 38 | private extension SKDetectingImageView { 39 | func setup() { 40 | isUserInteractionEnabled = true 41 | 42 | let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:))) 43 | doubleTap.numberOfTapsRequired = 2 44 | addGestureRecognizer(doubleTap) 45 | 46 | let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleSingleTap(_:))) 47 | singleTap.require(toFail: doubleTap) 48 | addGestureRecognizer(singleTap) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /SKPhotoBrowser/extensions/ObjC/UIImage+animatedGIF.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | UIImage (animatedGIF) 5 | 6 | This category adds class methods to `UIImage` to create an animated `UIImage` from an animated GIF. 7 | */ 8 | @interface UIImage (animatedGIF) 9 | 10 | /* 11 | UIImage *animation = [UIImage animatedImageWithAnimatedGIFData:theData]; 12 | 13 | I interpret `theData` as a GIF. I create an animated `UIImage` using the source images in the GIF. 14 | 15 | The GIF stores a separate duration for each frame, in units of centiseconds (hundredths of a second). However, a `UIImage` only has a single, total `duration` property, which is a floating-point number. 16 | 17 | To handle this mismatch, I add each source image (from the GIF) to `animation` a varying number of times to match the ratios between the frame durations in the GIF. 18 | 19 | For example, suppose the GIF contains three frames. Frame 0 has duration 3. Frame 1 has duration 9. Frame 2 has duration 15. I divide each duration by the greatest common denominator of all the durations, which is 3, and add each frame the resulting number of times. Thus `animation` will contain frame 0 3/3 = 1 time, then frame 1 9/3 = 3 times, then frame 2 15/3 = 5 times. I set `animation.duration` to (3+9+15)/100 = 0.27 seconds. 20 | */ 21 | + (UIImage * _Nullable)animatedImageWithAnimatedGIFData:(NSData * _Nonnull)theData; 22 | 23 | /* 24 | UIImage *image = [UIImage animatedImageWithAnimatedGIFURL:theURL]; 25 | 26 | I interpret the contents of `theURL` as a GIF. I create an animated `UIImage` using the source images in the GIF. 27 | 28 | I operate exactly like `+[UIImage animatedImageWithAnimatedGIFData:]`, except that I read the data from `theURL`. If `theURL` is not a `file:` URL, you probably want to call me on a background thread or GCD queue to avoid blocking the main thread. 29 | */ 30 | + (UIImage * _Nullable)animatedImageWithAnimatedGIFURL:(NSURL * _Nonnull)theURL; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPhotoLibraryUsageDescription 6 | for example, this app accesses the user’s photo library 7 | CFBundleDevelopmentRegion 8 | en 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 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UIStatusBarHidden 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | UIInterfaceOrientationPortraitUpsideDown 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | NSAppTransportSecurity 52 | 53 | NSAllowsArbitraryLoads 54 | 55 | 56 | NSPhotoLibraryUsageDescription 57 | Used to get photos 58 | NSCameraUsageDescription 59 | Used to take photos 60 | 61 | 62 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKLocalPhoto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKLocalPhoto.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by Antoine Barrault on 13/04/2016. 6 | // Copyright © 2016 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - SKLocalPhoto 12 | open class SKLocalPhoto: NSObject, SKPhotoProtocol { 13 | 14 | open var underlyingImage: UIImage! 15 | open var photoURL: String! 16 | open var contentMode: UIView.ContentMode = .scaleToFill 17 | open var shouldCachePhotoURLImage: Bool = false 18 | open var caption: String? 19 | open var index: Int = 0 20 | 21 | override init() { 22 | super.init() 23 | } 24 | 25 | convenience init(url: String) { 26 | self.init() 27 | photoURL = url 28 | } 29 | 30 | convenience init(url: String, holder: UIImage?) { 31 | self.init() 32 | photoURL = url 33 | underlyingImage = holder 34 | } 35 | 36 | open func checkCache() {} 37 | 38 | open func loadUnderlyingImageAndNotify() { 39 | 40 | if underlyingImage != nil && photoURL == nil { 41 | loadUnderlyingImageComplete() 42 | } 43 | 44 | if photoURL != nil { 45 | // Fetch Image 46 | if FileManager.default.fileExists(atPath: photoURL) { 47 | if let data = FileManager.default.contents(atPath: photoURL) { 48 | self.loadUnderlyingImageComplete() 49 | if let image = UIImage(data: data) { 50 | self.underlyingImage = image 51 | self.loadUnderlyingImageComplete() 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | open func loadUnderlyingImageComplete() { 59 | NotificationCenter.default.post(name: Notification.Name(rawValue: SKPHOTO_LOADING_DID_END_NOTIFICATION), object: self) 60 | } 61 | 62 | // MARK: - class func 63 | open class func photoWithImageURL(_ url: String) -> SKLocalPhoto { 64 | return SKLocalPhoto(url: url) 65 | } 66 | 67 | open class func photoWithImageURL(_ url: String, holder: UIImage?) -> SKLocalPhoto { 68 | return SKLocalPhoto(url: url, holder: holder) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKToolbar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKToolbar.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by keishi_suzuki on 2017/12/20. 6 | // Copyright © 2017年 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // helpers which often used 12 | private let bundle = Bundle(for: SKPhotoBrowser.self) 13 | 14 | class SKToolbar: UIToolbar { 15 | var toolActionButton: UIBarButtonItem! 16 | fileprivate weak var browser: SKPhotoBrowser? 17 | 18 | required init?(coder aDecoder: NSCoder) { 19 | super.init(coder: aDecoder) 20 | } 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | } 25 | 26 | convenience init(frame: CGRect, browser: SKPhotoBrowser) { 27 | self.init(frame: frame) 28 | self.browser = browser 29 | 30 | setupApperance() 31 | setupToolbar() 32 | } 33 | 34 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 35 | if let view = super.hitTest(point, with: event) { 36 | if SKMesurement.screenWidth - point.x < 50 { // FIXME: not good idea 37 | return view 38 | } 39 | } 40 | return nil 41 | } 42 | } 43 | 44 | private extension SKToolbar { 45 | func setupApperance() { 46 | backgroundColor = .clear 47 | clipsToBounds = true 48 | isTranslucent = true 49 | setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default) 50 | 51 | if #available(iOS 13.0, *) { 52 | let appearance = UIToolbarAppearance() 53 | appearance.configureWithTransparentBackground() 54 | standardAppearance = appearance 55 | compactAppearance = appearance 56 | if #available(iOS 15.0, *) { 57 | scrollEdgeAppearance = appearance 58 | } 59 | } else { 60 | isTranslucent = true 61 | setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default) 62 | setShadowImage(UIImage(), forToolbarPosition: .any) 63 | } 64 | } 65 | 66 | func setupToolbar() { 67 | toolActionButton = UIBarButtonItem(barButtonSystemItem: .action, target: browser, action: #selector(SKPhotoBrowser.actionButtonPressed)) 68 | toolActionButton.tintColor = UIColor.white 69 | 70 | var items = [UIBarButtonItem]() 71 | items.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)) 72 | if SKPhotoBrowserOptions.displayAction { 73 | items.append(toolActionButton) 74 | } 75 | setItems(items, animated: false) 76 | } 77 | 78 | func setupActionButton() { 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /SKPhotoBrowserTests/SKCacheTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKCacheTests.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by Kevin Wolkober on 6/13/16. 6 | // Copyright © 2016 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SKPhotoBrowser 11 | 12 | class SKCacheTests: XCTestCase { 13 | 14 | var cache: SKCache! 15 | let image = UIImage() 16 | let key = "test_image" 17 | 18 | override func setUp() { 19 | super.setUp() 20 | 21 | self.cache = SKCache() 22 | } 23 | 24 | override func tearDown() { 25 | self.cache = nil 26 | 27 | super.tearDown() 28 | } 29 | 30 | func testInit() { 31 | XCTAssertNotNil(self.cache.imageCache) 32 | XCTAssert(self.cache.imageCache is SKDefaultImageCache, "Default image cache should be loaded on init") 33 | } 34 | 35 | func testDefaultCacheImageForKey() { 36 | // given 37 | let cache = (self.cache.imageCache as? SKDefaultImageCache)!.cache 38 | cache.setObject(self.image, forKey: self.key as AnyObject) 39 | 40 | // when 41 | let cachedImage = self.cache.imageForKey(self.key) 42 | 43 | // then 44 | XCTAssertNotNil(cachedImage) 45 | } 46 | 47 | func testDefaultCacheSetImageForKey() { 48 | // when 49 | self.cache.setImage(self.image, forKey: self.key) 50 | 51 | // then 52 | let cache = (self.cache.imageCache as? SKDefaultImageCache)!.cache 53 | let cachedImage = cache.object(forKey: self.key as AnyObject) as? UIImage 54 | XCTAssertNotNil(cachedImage) 55 | } 56 | 57 | func testDefaultCacheRemoveImageForKey() { 58 | // given 59 | let cache = (self.cache.imageCache as? SKDefaultImageCache)!.cache 60 | cache.setObject(self.image, forKey: self.key as AnyObject) 61 | 62 | // when 63 | self.cache.removeImageForKey(self.key) 64 | 65 | // then 66 | let cachedImage = self.cache.imageForKey(self.key) 67 | XCTAssertNil(cachedImage) 68 | } 69 | 70 | func testDefaultCacheRemoveAllImages() { 71 | // given 72 | let cache = (self.cache.imageCache as? SKDefaultImageCache)!.cache 73 | cache.setObject(self.image, forKey: self.key as AnyObject) 74 | 75 | let anotherImage = UIImage() 76 | let anotherKey = "another_test_image" 77 | cache.setObject(anotherImage, forKey: anotherKey as AnyObject) 78 | 79 | // when 80 | self.cache.removeAllImages() 81 | 82 | // then 83 | let cachedImage = self.cache.imageForKey(self.key) 84 | let anotherCachedImage = self.cache.imageForKey(anotherKey) 85 | XCTAssertNil(cachedImage) 86 | XCTAssertNil(anotherCachedImage) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKCache.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by Kevin Wolkober on 6/13/16. 6 | // Copyright © 2016 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SKCache { 12 | public static let sharedCache = SKCache() 13 | open var imageCache: SKCacheable 14 | 15 | init() { 16 | self.imageCache = SKDefaultImageCache() 17 | } 18 | 19 | open func imageForKey(_ key: String) -> UIImage? { 20 | guard let cache = imageCache as? SKImageCacheable else { 21 | return nil 22 | } 23 | 24 | return cache.imageForKey(key) 25 | } 26 | 27 | open func setImage(_ image: UIImage, forKey key: String) { 28 | guard let cache = imageCache as? SKImageCacheable else { 29 | return 30 | } 31 | 32 | cache.setImage(image, forKey: key) 33 | } 34 | 35 | open func removeImageForKey(_ key: String) { 36 | guard let cache = imageCache as? SKImageCacheable else { 37 | return 38 | } 39 | 40 | cache.removeImageForKey(key) 41 | } 42 | 43 | open func removeAllImages() { 44 | guard let cache = imageCache as? SKImageCacheable else { 45 | return 46 | } 47 | 48 | cache.removeAllImages() 49 | } 50 | 51 | open func imageForRequest(_ request: URLRequest) -> UIImage? { 52 | guard let cache = imageCache as? SKRequestResponseCacheable else { 53 | return nil 54 | } 55 | 56 | if let response = cache.cachedResponseForRequest(request) { 57 | return UIImage(data: response.data) 58 | } 59 | return nil 60 | } 61 | 62 | open func setImageData(_ data: Data, response: URLResponse, request: URLRequest?) { 63 | guard let cache = imageCache as? SKRequestResponseCacheable, let request = request else { 64 | return 65 | } 66 | let cachedResponse = CachedURLResponse(response: response, data: data) 67 | cache.storeCachedResponse(cachedResponse, forRequest: request) 68 | } 69 | } 70 | 71 | class SKDefaultImageCache: SKImageCacheable { 72 | var cache: NSCache 73 | 74 | init() { 75 | cache = NSCache() 76 | } 77 | 78 | func imageForKey(_ key: String) -> UIImage? { 79 | return cache.object(forKey: key as AnyObject) as? UIImage 80 | } 81 | 82 | func setImage(_ image: UIImage, forKey key: String) { 83 | cache.setObject(image, forKey: key as AnyObject) 84 | } 85 | 86 | func removeImageForKey(_ key: String) { 87 | cache.removeObject(forKey: key as AnyObject) 88 | } 89 | 90 | func removeAllImages() { 91 | cache.removeAllObjects() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKCaptionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKCaptionView.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by suzuki_keishi on 2015/10/07. 6 | // Copyright © 2015 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SKCaptionView: UIView { 12 | fileprivate var photo: SKPhotoProtocol? 13 | fileprivate var photoLabel: UILabel! 14 | fileprivate var photoLabelPadding: CGFloat = 10 15 | 16 | required public init?(coder aDecoder: NSCoder) { 17 | super.init(coder: aDecoder) 18 | } 19 | 20 | public override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | } 23 | 24 | public convenience init(photo: SKPhotoProtocol) { 25 | self.init(frame: CGRect(x: 0, y: 0, width: SKMesurement.screenWidth, height: SKMesurement.screenHeight)) 26 | self.photo = photo 27 | setup() 28 | } 29 | 30 | open override func sizeThatFits(_ size: CGSize) -> CGSize { 31 | guard let text = photoLabel.text, text.count > 0 else { 32 | return CGSize.zero 33 | } 34 | 35 | let font: UIFont = photoLabel.font 36 | let width: CGFloat = size.width - photoLabelPadding * 2 37 | let height: CGFloat = photoLabel.font.lineHeight * CGFloat(photoLabel.numberOfLines) 38 | 39 | let attributedText = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: font]) 40 | let textSize = attributedText.boundingRect(with: CGSize(width: width, height: height), options: .usesLineFragmentOrigin, context: nil).size 41 | 42 | return CGSize(width: textSize.width, height: textSize.height + photoLabelPadding * 2) 43 | } 44 | } 45 | 46 | private extension SKCaptionView { 47 | func setup() { 48 | isOpaque = false 49 | autoresizingMask = [.flexibleWidth, .flexibleTopMargin, .flexibleRightMargin, .flexibleLeftMargin] 50 | 51 | // setup photoLabel 52 | setupPhotoLabel() 53 | } 54 | 55 | func setupPhotoLabel() { 56 | photoLabel = UILabel(frame: CGRect(x: photoLabelPadding, y: 0, width: bounds.size.width - (photoLabelPadding * 2), height: bounds.size.height)) 57 | photoLabel.autoresizingMask = [.flexibleWidth, .flexibleHeight] 58 | photoLabel.isOpaque = false 59 | photoLabel.backgroundColor = SKCaptionOptions.backgroundColor 60 | photoLabel.textColor = SKCaptionOptions.textColor 61 | photoLabel.textAlignment = SKCaptionOptions.textAlignment 62 | photoLabel.lineBreakMode = SKCaptionOptions.lineBreakMode 63 | photoLabel.numberOfLines = SKCaptionOptions.numberOfLine 64 | photoLabel.font = SKCaptionOptions.font 65 | photoLabel.shadowColor = UIColor(white: 0.0, alpha: 0.5) 66 | photoLabel.shadowOffset = CGSize(width: 0.0, height: 1.0) 67 | photoLabel.text = photo?.caption 68 | 69 | addSubview(photoLabel) 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /SKPhotoBrowser/extensions/UIImage+Rotation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Rotation.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by K Rummler on 15/03/16. 6 | // Copyright © 2016 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | func rotateImageByOrientation() -> UIImage { 13 | // No-op if the orientation is already correct 14 | guard self.imageOrientation != .up else { 15 | return self 16 | } 17 | 18 | let transform = calculateAffineTransform() 19 | 20 | // Now we draw the underlying CGImage into a new context, applying the transform 21 | // calculated above. 22 | let ctx = CGContext(data: nil, width: Int(self.size.width), height: Int(self.size.height), 23 | bitsPerComponent: self.cgImage!.bitsPerComponent, bytesPerRow: 0, 24 | space: self.cgImage!.colorSpace!, 25 | bitmapInfo: self.cgImage!.bitmapInfo.rawValue) 26 | ctx!.concatenate(transform) 27 | 28 | switch self.imageOrientation { 29 | case .left, .leftMirrored, .right, .rightMirrored: 30 | ctx!.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) 31 | 32 | default: 33 | ctx!.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) 34 | } 35 | 36 | // And now we just create a new UIImage from the drawing context 37 | if let cgImage = ctx!.makeImage() { 38 | return UIImage(cgImage: cgImage) 39 | } else { 40 | return self 41 | } 42 | } 43 | 44 | fileprivate func calculateAffineTransform() -> CGAffineTransform { 45 | // We need to calculate the proper transformation to make the image upright. 46 | // We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored. 47 | var transform = CGAffineTransform.identity 48 | 49 | switch self.imageOrientation { 50 | case .down, .downMirrored: 51 | transform = transform.translatedBy(x: self.size.width, y: self.size.height) 52 | transform = transform.rotated(by: .pi) 53 | 54 | case .left, .leftMirrored: 55 | transform = transform.translatedBy(x: self.size.width, y: 0) 56 | transform = transform.rotated(by: .pi / 2) 57 | 58 | case .right, .rightMirrored: 59 | transform = transform.translatedBy(x: 0, y: self.size.height) 60 | transform = transform.rotated(by: -.pi / 2) 61 | 62 | default: 63 | break 64 | } 65 | 66 | switch self.imageOrientation { 67 | case .upMirrored, .downMirrored: 68 | transform = transform.translatedBy(x: self.size.width, y: 0) 69 | transform = transform.scaledBy(x: -1, y: 1) 70 | 71 | case .leftMirrored, .rightMirrored: 72 | transform = transform.translatedBy(x: self.size.height, y: 0) 73 | transform = transform.scaledBy(x: -1, y: 1) 74 | 75 | default: 76 | break 77 | } 78 | 79 | return transform 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /SKPhotoBrowser.xcodeproj/xcshareddata/xcschemes/SKPhotoBrowser.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/FromWebViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FromWebViewController.swift 3 | // SKPhotoBrowserExample 4 | // 5 | // Created by suzuki_keishi on 2015/10/06. 6 | // Copyright © 2015 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SKPhotoBrowser 11 | import SDWebImage 12 | 13 | class FromWebViewController: UIViewController, SKPhotoBrowserDelegate { 14 | var images = [SKPhotoProtocol]() 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | SKCache.sharedCache.imageCache = CustomImageCache() 20 | } 21 | 22 | @IBAction func pushJpgPngButton(_ sender: AnyObject) { 23 | let browser = SKPhotoBrowser(photos: createWebPhotos()) 24 | browser.initializePageIndex(0) 25 | browser.delegate = self 26 | 27 | present(browser, animated: true, completion: nil) 28 | } 29 | 30 | @IBAction func pushGifButton(_ sender: AnyObject) { 31 | let browser = SKPhotoBrowser(photos: createWebGifPhotos()) 32 | browser.initializePageIndex(0) 33 | browser.delegate = self 34 | 35 | present(browser, animated: true, completion: nil) 36 | } 37 | } 38 | 39 | // MARK: - SKPhotoBrowserDelegate 40 | 41 | extension FromWebViewController { 42 | func didDismissAtPageIndex(_ index: Int) { 43 | } 44 | 45 | func didDismissActionSheetWithButtonIndex(_ buttonIndex: Int, photoIndex: Int) { 46 | } 47 | 48 | func removePhoto(index: Int, reload: (() -> Void)) { 49 | SKCache.sharedCache.removeImageForKey("somekey") 50 | reload() 51 | } 52 | } 53 | 54 | // MARK: - private 55 | 56 | private extension FromWebViewController { 57 | func createWebPhotos() -> [SKPhotoProtocol] { 58 | return (0..<10).map { (i: Int) -> SKPhotoProtocol in 59 | let photo = SKPhoto.photoWithImageURL("https://placehold.jp/15\(i)x15\(i).png") 60 | photo.caption = caption[i%10] 61 | photo.shouldCachePhotoURLImage = true 62 | return photo 63 | } 64 | } 65 | 66 | func createWebGifPhotos() -> [SKPhotoProtocol] { 67 | let gifs: [String] = [ 68 | "https://media.giphy.com/media/ftdEPX8jF6SvC/giphy.gif", 69 | "https://media.giphy.com/media/MDrq4Gwd0i4m4mGwCr/giphy.gif", 70 | "https://media.giphy.com/media/jOOjdVK9i2Qn1alT6a/giphy.gif" 71 | ] 72 | 73 | var result: [SKPhotoProtocol] = [] 74 | for (i, v) in gifs.enumerated() { 75 | let photo = SKPhoto.photoWithImageURL(v) 76 | photo.caption = caption[i] 77 | result.append(photo) 78 | } 79 | return result 80 | } 81 | } 82 | 83 | class CustomImageCache: SKImageCacheable { 84 | var cache: SDImageCache? 85 | 86 | init() { 87 | let cache = SDImageCache(namespace: "com.suzuki.custom.cache") 88 | } 89 | 90 | func imageForKey(_ key: String) -> UIImage? { return cache?.imageFromDiskCache(forKey: key) } 91 | 92 | func setImage(_ image: UIImage, forKey key: String) { cache?.store(image, forKey: key) } 93 | 94 | func removeImageForKey(_ key: String) {} 95 | 96 | func removeAllImages() {} 97 | 98 | } 99 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowserOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKPhotoBrowserOptions.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by 鈴木 啓司 on 2016/08/18. 6 | // Copyright © 2016年 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct SKPhotoBrowserOptions { 12 | public static var displayStatusbar: Bool = false 13 | public static var displayCloseButton: Bool = true 14 | public static var displayDeleteButton: Bool = false 15 | 16 | public static var displayAction: Bool = true 17 | public static var shareExtraCaption: String? 18 | public static var actionButtonTitles: [String]? 19 | 20 | public static var displayCounterLabel: Bool = true 21 | public static var displayBackAndForwardButton: Bool = true 22 | 23 | public static var displayHorizontalScrollIndicator: Bool = true 24 | public static var displayVerticalScrollIndicator: Bool = true 25 | public static var displayPagingHorizontalScrollIndicator: Bool = true 26 | 27 | public static var bounceAnimation: Bool = false 28 | public static var enableZoomBlackArea: Bool = true 29 | public static var enableSingleTapDismiss: Bool = false 30 | 31 | public static var backgroundColor: UIColor = .black 32 | public static var indicatorColor: UIColor = .white 33 | public static var indicatorStyle: UIActivityIndicatorView.Style = .whiteLarge 34 | 35 | /// By default close button is on left side and delete button is on right. 36 | /// 37 | /// Set this property to **true** for swap they. 38 | /// 39 | /// Default: false 40 | public static var swapCloseAndDeleteButtons: Bool = false 41 | public static var disableVerticalSwipe: Bool = false 42 | 43 | /// if this value is true, the long photo width will match the screen, 44 | /// and the minScale is 1.0, the maxScale is 2.5 45 | /// Default: false 46 | public static var longPhotoWidthMatchScreen: Bool = false 47 | 48 | /// Provide custom session configuration (eg. for headers, etc.) 49 | public static var sessionConfiguration: URLSessionConfiguration = .default 50 | 51 | /// if this value is true, when you take a screenshot, the image will be hidden 52 | /// only working on a device, not on simulator 53 | public static var protectScreenshot: Bool = false 54 | } 55 | 56 | public struct SKButtonOptions { 57 | public static var closeButtonPadding: CGPoint = CGPoint(x: 5, y: 20) 58 | public static var deleteButtonPadding: CGPoint = CGPoint(x: 5, y: 20) 59 | } 60 | 61 | public struct SKCaptionOptions { 62 | public enum CaptionLocation { 63 | case basic 64 | case bottom 65 | } 66 | 67 | public static var textColor: UIColor = .white 68 | public static var textAlignment: NSTextAlignment = .center 69 | public static var numberOfLine: Int = 3 70 | public static var lineBreakMode: NSLineBreakMode = .byTruncatingTail 71 | public static var font: UIFont = .systemFont(ofSize: 17.0) 72 | public static var backgroundColor: UIColor = .clear 73 | public static var captionLocation: CaptionLocation = .basic 74 | } 75 | 76 | public struct SKToolbarOptions { 77 | public static var textColor: UIColor = .white 78 | public static var font: UIFont = .systemFont(ofSize: 17.0) 79 | public static var textShadowColor: UIColor = .black 80 | } 81 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhotoBrowserDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKPhotoBrowserDelegate.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by 鈴木 啓司 on 2016/08/09. 6 | // Copyright © 2016年 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol SKPhotoBrowserDelegate { 12 | 13 | /** 14 | Tells the delegate that the browser started displaying a new photo 15 | 16 | - Parameter index: the index of the new photo 17 | */ 18 | @objc optional func didShowPhotoAtIndex(_ browser: SKPhotoBrowser, index: Int) 19 | 20 | /** 21 | Tells the delegate the browser will start to dismiss 22 | 23 | - Parameter index: the index of the current photo 24 | */ 25 | @objc optional func willDismissAtPageIndex(_ index: Int) 26 | 27 | /** 28 | Tells the delegate that the browser will start showing the `UIActionSheet` 29 | 30 | - Parameter photoIndex: the index of the current photo 31 | */ 32 | @objc optional func willShowActionSheet(_ photoIndex: Int) 33 | 34 | /** 35 | Tells the delegate that the browser has been dismissed 36 | 37 | - Parameter index: the index of the current photo 38 | */ 39 | @objc optional func didDismissAtPageIndex(_ index: Int) 40 | 41 | /** 42 | Tells the delegate that the browser did dismiss the UIActionSheet 43 | 44 | - Parameter buttonIndex: the index of the pressed button 45 | - Parameter photoIndex: the index of the current photo 46 | */ 47 | @objc optional func didDismissActionSheetWithButtonIndex(_ buttonIndex: Int, photoIndex: Int) 48 | 49 | /** 50 | Tells the delegate that the browser did scroll to index 51 | 52 | - Parameter index: the index of the photo where the user had scroll 53 | */ 54 | @objc optional func didScrollToIndex(_ browser: SKPhotoBrowser, index: Int) 55 | 56 | /** 57 | Tells the delegate the user removed a photo, when implementing this call, be sure to call reload to finish the deletion process 58 | 59 | - Parameter browser: reference to the calling SKPhotoBrowser 60 | - Parameter index: the index of the removed photo 61 | - Parameter reload: function that needs to be called after finishing syncing up 62 | */ 63 | @objc optional func removePhoto(_ browser: SKPhotoBrowser, index: Int, reload: @escaping (() -> Void)) 64 | 65 | /** 66 | Asks the delegate for the view for a certain photo. Needed to detemine the animation when presenting/closing the browser. 67 | 68 | - Parameter browser: reference to the calling SKPhotoBrowser 69 | - Parameter index: the index of the removed photo 70 | 71 | - Returns: the view to animate to 72 | */ 73 | @objc optional func viewForPhoto(_ browser: SKPhotoBrowser, index: Int) -> UIView? 74 | 75 | /** 76 | Tells the delegate that the controls view toggled visibility 77 | 78 | - Parameter browser: reference to the calling SKPhotoBrowser 79 | - Parameter hidden: the status of visibility control 80 | */ 81 | @objc optional func controlsVisibilityToggled(_ browser: SKPhotoBrowser, hidden: Bool) 82 | 83 | /** 84 | Allows the delegate to create its own caption view 85 | 86 | - Parameter index: the index of the photo 87 | */ 88 | @objc optional func captionViewForPhotoAtIndex(index: Int) -> SKCaptionView? 89 | } 90 | 91 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKButtons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKButtons.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by 鈴木 啓司 on 2016/08/09. 6 | // Copyright © 2016年 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SKButton: UIButton { 12 | internal var showFrame: CGRect! 13 | internal var hideFrame: CGRect! 14 | 15 | fileprivate var insets: UIEdgeInsets { 16 | if SKMesurement.isPhone { 17 | return UIEdgeInsets(top: 15.25, left: 15.25, bottom: 15.25, right: 15.25) 18 | } else { 19 | return UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) 20 | } 21 | } 22 | fileprivate let size: CGSize = CGSize(width: 44, height: 44) 23 | fileprivate var marginX: CGFloat = 0 24 | fileprivate var marginY: CGFloat = 0 25 | fileprivate var extraMarginY: CGFloat = 20 //NOTE: dynamic to static 26 | 27 | func setup(_ imageName: String) { 28 | backgroundColor = .clear 29 | imageEdgeInsets = insets 30 | translatesAutoresizingMaskIntoConstraints = true 31 | autoresizingMask = [.flexibleBottomMargin, .flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin] 32 | 33 | setImage(UIImage.bundledImage(named: imageName), for: .normal) 34 | } 35 | 36 | func setFrameSize(_ size: CGSize? = nil) { 37 | guard let size = size else { return } 38 | 39 | let newRect = CGRect(x: marginX, y: marginY, width: size.width, height: size.height) 40 | frame = newRect 41 | showFrame = newRect 42 | hideFrame = CGRect(x: marginX, y: -marginY, width: size.width, height: size.height) 43 | } 44 | 45 | func updateFrame(_ frameSize: CGSize) { } 46 | } 47 | 48 | class SKImageButton: SKButton { 49 | fileprivate var imageName: String { return "" } 50 | 51 | required init?(coder aDecoder: NSCoder) { 52 | super.init(coder: aDecoder) 53 | } 54 | 55 | override init(frame: CGRect) { 56 | super.init(frame: frame) 57 | setup(imageName) 58 | showFrame = CGRect(x: marginX, y: marginY, width: size.width, height: size.height) 59 | hideFrame = CGRect(x: marginX, y: -marginY, width: size.width, height: size.height) 60 | } 61 | } 62 | 63 | class SKCloseButton: SKImageButton { 64 | override var imageName: String { return "btn_common_close_wh" } 65 | override var marginX: CGFloat { 66 | get { 67 | return SKPhotoBrowserOptions.swapCloseAndDeleteButtons 68 | ? SKMesurement.screenWidth - SKButtonOptions.closeButtonPadding.x - self.size.width 69 | : SKButtonOptions.closeButtonPadding.x 70 | } 71 | set { super.marginX = newValue } 72 | } 73 | override var marginY: CGFloat { 74 | get { return SKButtonOptions.closeButtonPadding.y + extraMarginY } 75 | set { super.marginY = newValue } 76 | } 77 | 78 | required init?(coder aDecoder: NSCoder) { 79 | super.init(coder: aDecoder) 80 | } 81 | 82 | override init(frame: CGRect) { 83 | super.init(frame: frame) 84 | setup(imageName) 85 | showFrame = CGRect(x: marginX, y: marginY, width: size.width, height: size.height) 86 | hideFrame = CGRect(x: marginX, y: -marginY, width: size.width, height: size.height) 87 | } 88 | } 89 | 90 | class SKDeleteButton: SKImageButton { 91 | override var imageName: String { return "btn_common_delete_wh" } 92 | override var marginX: CGFloat { 93 | get { 94 | return SKPhotoBrowserOptions.swapCloseAndDeleteButtons 95 | ? SKButtonOptions.deleteButtonPadding.x 96 | : SKMesurement.screenWidth - SKButtonOptions.deleteButtonPadding.x - self.size.width 97 | } 98 | set { super.marginX = newValue } 99 | } 100 | override var marginY: CGFloat { 101 | get { return SKButtonOptions.deleteButtonPadding.y + extraMarginY } 102 | set { super.marginY = newValue } 103 | } 104 | 105 | required init?(coder aDecoder: NSCoder) { 106 | super.init(coder: aDecoder) 107 | } 108 | 109 | override init(frame: CGRect) { 110 | super.init(frame: frame) 111 | setup(imageName) 112 | showFrame = CGRect(x: marginX, y: marginY, width: size.width, height: size.height) 113 | hideFrame = CGRect(x: marginX, y: -marginY, width: size.width, height: size.height) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKActionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKOptionalActionView.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by keishi_suzuki on 2017/12/19. 6 | // Copyright © 2017年 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SKActionView: UIView { 12 | internal weak var browser: SKPhotoBrowser? 13 | internal var closeButton: SKCloseButton! 14 | internal var deleteButton: SKDeleteButton! 15 | 16 | // Action 17 | fileprivate var cancelTitle = "Cancel" 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | } 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | } 26 | 27 | convenience init(frame: CGRect, browser: SKPhotoBrowser) { 28 | self.init(frame: frame) 29 | self.browser = browser 30 | 31 | configureCloseButton() 32 | configureDeleteButton() 33 | } 34 | 35 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 36 | if let view = super.hitTest(point, with: event) { 37 | if closeButton.frame.contains(point) || deleteButton.frame.contains(point) { 38 | return view 39 | } 40 | return nil 41 | } 42 | return nil 43 | } 44 | 45 | func updateFrame(frame: CGRect) { 46 | self.frame = frame 47 | setNeedsDisplay() 48 | } 49 | 50 | func updateCloseButton(image: UIImage, size: CGSize? = nil) { 51 | configureCloseButton(image: image, size: size) 52 | } 53 | 54 | func updateDeleteButton(image: UIImage, size: CGSize? = nil) { 55 | configureDeleteButton(image: image, size: size) 56 | } 57 | 58 | func animate(hidden: Bool) { 59 | let closeFrame: CGRect = hidden ? closeButton.hideFrame : closeButton.showFrame 60 | let deleteFrame: CGRect = hidden ? deleteButton.hideFrame : deleteButton.showFrame 61 | UIView.animate(withDuration: 0.35, 62 | animations: { () -> Void in 63 | let alpha: CGFloat = hidden ? 0.0 : 1.0 64 | 65 | if SKPhotoBrowserOptions.displayCloseButton { 66 | self.closeButton.alpha = alpha 67 | self.closeButton.frame = closeFrame 68 | } 69 | if SKPhotoBrowserOptions.displayDeleteButton { 70 | self.deleteButton.alpha = alpha 71 | self.deleteButton.frame = deleteFrame 72 | } 73 | }, completion: nil) 74 | } 75 | 76 | @objc func closeButtonPressed(_ sender: UIButton) { 77 | browser?.determineAndClose() 78 | } 79 | 80 | @objc func deleteButtonPressed(_ sender: UIButton) { 81 | guard let browser = self.browser else { return } 82 | 83 | browser.delegate?.removePhoto?(browser, index: browser.currentPageIndex) { [weak self] in 84 | self?.browser?.deleteImage() 85 | } 86 | } 87 | } 88 | 89 | extension SKActionView { 90 | func configureCloseButton(image: UIImage? = nil, size: CGSize? = nil) { 91 | if closeButton == nil { 92 | closeButton = SKCloseButton(frame: .zero) 93 | closeButton.addTarget(self, action: #selector(closeButtonPressed(_:)), for: .touchUpInside) 94 | closeButton.isHidden = !SKPhotoBrowserOptions.displayCloseButton 95 | addSubview(closeButton) 96 | } 97 | 98 | if let size = size { 99 | closeButton.setFrameSize(size) 100 | } 101 | 102 | if let image = image { 103 | closeButton.setImage(image, for: .normal) 104 | } 105 | } 106 | 107 | func configureDeleteButton(image: UIImage? = nil, size: CGSize? = nil) { 108 | if deleteButton == nil { 109 | deleteButton = SKDeleteButton(frame: .zero) 110 | deleteButton.addTarget(self, action: #selector(deleteButtonPressed(_:)), for: .touchUpInside) 111 | deleteButton.isHidden = !SKPhotoBrowserOptions.displayDeleteButton 112 | addSubview(deleteButton) 113 | } 114 | 115 | if let size = size { 116 | deleteButton.setFrameSize(size) 117 | } 118 | 119 | if let image = image { 120 | deleteButton.setImage(image, for: .normal) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPhoto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKPhoto.swift 3 | // SKViewExample 4 | // 5 | // Created by suzuki_keishi on 2015/10/01. 6 | // Copyright © 2015 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | #if canImport(SKPhotoBrowserObjC) 11 | import SKPhotoBrowserObjC 12 | #endif 13 | 14 | @objc public protocol SKPhotoProtocol: NSObjectProtocol { 15 | var index: Int { get set } 16 | var underlyingImage: UIImage! { get } 17 | var caption: String? { get } 18 | var contentMode: UIView.ContentMode { get set } 19 | func loadUnderlyingImageAndNotify() 20 | func checkCache() 21 | } 22 | 23 | // MARK: - SKPhoto 24 | open class SKPhoto: NSObject, SKPhotoProtocol { 25 | open var index: Int = 0 26 | open var underlyingImage: UIImage! 27 | open var caption: String? 28 | open var contentMode: UIView.ContentMode = .scaleAspectFill 29 | open var shouldCachePhotoURLImage: Bool = false 30 | open var photoURL: String! 31 | 32 | override init() { 33 | super.init() 34 | } 35 | 36 | convenience init(image: UIImage) { 37 | self.init() 38 | underlyingImage = image 39 | } 40 | 41 | convenience init(url: String) { 42 | self.init() 43 | photoURL = url 44 | } 45 | 46 | convenience init(url: String, holder: UIImage?) { 47 | self.init() 48 | photoURL = url 49 | underlyingImage = holder 50 | } 51 | 52 | open func checkCache() { 53 | guard let photoURL = photoURL else { 54 | return 55 | } 56 | guard shouldCachePhotoURLImage else { 57 | return 58 | } 59 | 60 | if SKCache.sharedCache.imageCache is SKRequestResponseCacheable { 61 | let request = URLRequest(url: URL(string: photoURL)!) 62 | if let img = SKCache.sharedCache.imageForRequest(request) { 63 | underlyingImage = img 64 | } 65 | } else { 66 | if let img = SKCache.sharedCache.imageForKey(photoURL) { 67 | underlyingImage = img 68 | } 69 | } 70 | } 71 | 72 | open func loadUnderlyingImageAndNotify() { 73 | guard photoURL != nil, let URL = URL(string: photoURL) else { return } 74 | 75 | if self.shouldCachePhotoURLImage { 76 | if SKCache.sharedCache.imageCache is SKRequestResponseCacheable { 77 | let request = URLRequest(url: URL) 78 | if let img = SKCache.sharedCache.imageForRequest(request) { 79 | DispatchQueue.main.async { 80 | self.underlyingImage = img 81 | self.loadUnderlyingImageComplete() 82 | } 83 | return 84 | } 85 | } else { 86 | if let img = SKCache.sharedCache.imageForKey(photoURL) { 87 | DispatchQueue.main.async { 88 | self.underlyingImage = img 89 | self.loadUnderlyingImageComplete() 90 | } 91 | return 92 | } 93 | } 94 | } 95 | 96 | // Fetch Image 97 | let session = URLSession(configuration: SKPhotoBrowserOptions.sessionConfiguration) 98 | var task: URLSessionTask? 99 | task = session.dataTask(with: URL, completionHandler: { [weak self] (data, response, error) in 100 | guard let self = self else { return } 101 | defer { session.finishTasksAndInvalidate() } 102 | 103 | guard error == nil else { 104 | DispatchQueue.main.async { 105 | self.loadUnderlyingImageComplete() 106 | } 107 | return 108 | } 109 | 110 | if let data = data, let response = response, let image = UIImage.animatedImage(withAnimatedGIFData: data) { 111 | if self.shouldCachePhotoURLImage { 112 | if SKCache.sharedCache.imageCache is SKRequestResponseCacheable { 113 | SKCache.sharedCache.setImageData(data, response: response, request: task?.originalRequest) 114 | } else { 115 | SKCache.sharedCache.setImage(image, forKey: self.photoURL) 116 | } 117 | } 118 | DispatchQueue.main.async { 119 | self.underlyingImage = image 120 | self.loadUnderlyingImageComplete() 121 | } 122 | } 123 | 124 | }) 125 | task?.resume() 126 | } 127 | 128 | open func loadUnderlyingImageComplete() { 129 | NotificationCenter.default.post(name: Notification.Name(rawValue: SKPHOTO_LOADING_DID_END_NOTIFICATION), object: self) 130 | } 131 | 132 | } 133 | 134 | // MARK: - Static Function 135 | 136 | extension SKPhoto { 137 | public static func photoWithImage(_ image: UIImage) -> SKPhoto { 138 | return SKPhoto(image: image) 139 | } 140 | 141 | public static func photoWithImageURL(_ url: String) -> SKPhoto { 142 | return SKPhoto(url: url) 143 | } 144 | 145 | public static func photoWithImageURL(_ url: String, holder: UIImage?) -> SKPhoto { 146 | return SKPhoto(url: url, holder: holder) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /SKPhotoBrowser/extensions/ObjC/UIImage+animatedGIF.m: -------------------------------------------------------------------------------- 1 | #import "UIImage+animatedGIF.h" 2 | #import 3 | 4 | #if __has_feature(objc_arc) 5 | #define toCF (__bridge CFTypeRef) 6 | #define fromCF (__bridge id) 7 | #else 8 | #define toCF (CFTypeRef) 9 | #define fromCF (id) 10 | #endif 11 | 12 | @implementation UIImage (animatedGIF) 13 | 14 | static int delayCentisecondsForImageAtIndex(CGImageSourceRef const source, size_t const i) { 15 | int delayCentiseconds = 1; 16 | CFDictionaryRef const properties = CGImageSourceCopyPropertiesAtIndex(source, i, NULL); 17 | if (properties) { 18 | CFDictionaryRef const gifProperties = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary); 19 | if (gifProperties) { 20 | NSNumber *number = fromCF CFDictionaryGetValue(gifProperties, kCGImagePropertyGIFUnclampedDelayTime); 21 | if (number == NULL || [number doubleValue] == 0) { 22 | number = fromCF CFDictionaryGetValue(gifProperties, kCGImagePropertyGIFDelayTime); 23 | } 24 | if ([number doubleValue] > 0) { 25 | // Even though the GIF stores the delay as an integer number of centiseconds, ImageIO “helpfully” converts that to seconds for us. 26 | delayCentiseconds = (int)lrint([number doubleValue] * 100); 27 | } 28 | } 29 | CFRelease(properties); 30 | } 31 | return delayCentiseconds; 32 | } 33 | 34 | static void createImagesAndDelays(CGImageSourceRef source, size_t count, CGImageRef imagesOut[count], int delayCentisecondsOut[count]) { 35 | for (size_t i = 0; i < count; ++i) { 36 | imagesOut[i] = CGImageSourceCreateImageAtIndex(source, i, NULL); 37 | delayCentisecondsOut[i] = delayCentisecondsForImageAtIndex(source, i); 38 | } 39 | } 40 | 41 | static int sum(size_t const count, int const *const values) { 42 | int theSum = 0; 43 | for (size_t i = 0; i < count; ++i) { 44 | theSum += values[i]; 45 | } 46 | return theSum; 47 | } 48 | 49 | static int pairGCD(int a, int b) { 50 | if (a < b) 51 | return pairGCD(b, a); 52 | while (true) { 53 | int const r = a % b; 54 | if (r == 0) 55 | return b; 56 | a = b; 57 | b = r; 58 | } 59 | } 60 | 61 | static int vectorGCD(size_t const count, int const *const values) { 62 | int gcd = values[0]; 63 | for (size_t i = 1; i < count; ++i) { 64 | // Note that after I process the first few elements of the vector, `gcd` will probably be smaller than any remaining element. By passing the smaller value as the second argument to `pairGCD`, I avoid making it swap the arguments. 65 | gcd = pairGCD(values[i], gcd); 66 | } 67 | return gcd; 68 | } 69 | 70 | static NSArray *frameArray(size_t const count, CGImageRef const images[count], int const delayCentiseconds[count], int const totalDurationCentiseconds) { 71 | int const gcd = vectorGCD(count, delayCentiseconds); 72 | size_t const frameCount = totalDurationCentiseconds / gcd; 73 | UIImage *frames[frameCount]; 74 | for (size_t i = 0, f = 0; i < count; ++i) { 75 | UIImage *const frame = [UIImage imageWithCGImage:images[i]]; 76 | for (size_t j = delayCentiseconds[i] / gcd; j > 0; --j) { 77 | frames[f++] = frame; 78 | } 79 | } 80 | return [NSArray arrayWithObjects:frames count:frameCount]; 81 | } 82 | 83 | static void releaseImages(size_t const count, CGImageRef const images[count]) { 84 | for (size_t i = 0; i < count; ++i) { 85 | CGImageRelease(images[i]); 86 | } 87 | } 88 | 89 | static UIImage *animatedImageWithAnimatedGIFImageSource(CGImageSourceRef const source) { 90 | size_t const count = CGImageSourceGetCount(source); 91 | CGImageRef images[count]; 92 | int delayCentiseconds[count]; // in centiseconds 93 | createImagesAndDelays(source, count, images, delayCentiseconds); 94 | int const totalDurationCentiseconds = sum(count, delayCentiseconds); 95 | NSArray *const frames = frameArray(count, images, delayCentiseconds, totalDurationCentiseconds); 96 | UIImage *const animation = [UIImage animatedImageWithImages:frames duration:(NSTimeInterval)totalDurationCentiseconds / 100.0]; 97 | releaseImages(count, images); 98 | return animation; 99 | } 100 | 101 | static UIImage *animatedImageWithAnimatedGIFReleasingImageSource(CGImageSourceRef CF_RELEASES_ARGUMENT source) { 102 | if (source) { 103 | UIImage *const image = animatedImageWithAnimatedGIFImageSource(source); 104 | CFRelease(source); 105 | return image; 106 | } else { 107 | return nil; 108 | } 109 | } 110 | 111 | static UIImage *animatedImageWithAnimatedGIFDataSource(CGImageSourceRef const source, NSData *data) { 112 | size_t const count = CGImageSourceGetCount(source); 113 | // If there's only one frame or less, process as a regular image to maintain correct orientation 114 | if(count <= 1) { 115 | return [UIImage imageWithData:data]; 116 | } 117 | 118 | // Continue using existing GIF animation handling logic for multi-frame images 119 | return animatedImageWithAnimatedGIFImageSource(source); 120 | } 121 | 122 | + (UIImage *)animatedImageWithAnimatedGIFData:(NSData *)data { 123 | return animatedImageWithAnimatedGIFDataSource(CGImageSourceCreateWithData(toCF data, NULL), data); 124 | } 125 | 126 | + (UIImage *)animatedImageWithAnimatedGIFURL:(NSURL *)url { 127 | return animatedImageWithAnimatedGIFReleasingImageSource(CGImageSourceCreateWithURL(toCF url, NULL)); 128 | } 129 | 130 | @end 131 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPaginationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKPaginationView.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by keishi_suzuki on 2017/12/20. 6 | // Copyright © 2017年 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SKPaginationView: UIView { 12 | var counterLabel: UILabel? 13 | var prevButton: UIButton? 14 | var nextButton: UIButton? 15 | private var margin: CGFloat = 100 16 | private var extraMargin: CGFloat = SKMesurement.isPhoneX ? 40 : 0 17 | 18 | fileprivate weak var browser: SKPhotoBrowser? 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | super.init(coder: aDecoder) 22 | } 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | } 27 | 28 | convenience init(frame: CGRect, browser: SKPhotoBrowser?) { 29 | self.init(frame: frame) 30 | self.frame = CGRect(x: 0, y: frame.height - margin - extraMargin, width: frame.width, height: 100) 31 | self.browser = browser 32 | 33 | setupApperance() 34 | setupCounterLabel() 35 | setupPrevButton() 36 | setupNextButton() 37 | 38 | update(browser?.currentPageIndex ?? 0) 39 | } 40 | 41 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 42 | if let view = super.hitTest(point, with: event) { 43 | if let counterLabel = counterLabel, counterLabel.frame.contains(point) { 44 | return view 45 | } else if let prevButton = prevButton, prevButton.frame.contains(point) { 46 | return view 47 | } else if let nextButton = nextButton, nextButton.frame.contains(point) { 48 | return view 49 | } 50 | return nil 51 | } 52 | return nil 53 | } 54 | 55 | func updateFrame(frame: CGRect) { 56 | self.frame = CGRect(x: 0, y: frame.height - margin, width: frame.width, height: 100) 57 | } 58 | 59 | func update(_ currentPageIndex: Int) { 60 | guard let browser = browser else { return } 61 | 62 | if browser.photos.count > 1 { 63 | counterLabel?.text = "\(currentPageIndex + 1) / \(browser.photos.count)" 64 | } else { 65 | counterLabel?.text = nil 66 | } 67 | 68 | guard let prevButton = prevButton, let nextButton = nextButton else { return } 69 | prevButton.isEnabled = (currentPageIndex > 0) 70 | nextButton.isEnabled = (currentPageIndex < browser.photos.count - 1) 71 | } 72 | 73 | func setControlsHidden(hidden: Bool) { 74 | let alpha: CGFloat = hidden ? 0.0 : 1.0 75 | 76 | UIView.animate(withDuration: 0.35, 77 | animations: { () -> Void in self.alpha = alpha }, 78 | completion: nil) 79 | } 80 | } 81 | 82 | private extension SKPaginationView { 83 | func setupApperance() { 84 | backgroundColor = .clear 85 | clipsToBounds = true 86 | } 87 | 88 | func setupCounterLabel() { 89 | guard SKPhotoBrowserOptions.displayCounterLabel else { return } 90 | 91 | let label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 50)) 92 | label.center = CGPoint(x: frame.width / 2, y: frame.height / 2) 93 | label.textAlignment = .center 94 | label.backgroundColor = .clear 95 | label.shadowColor = SKToolbarOptions.textShadowColor 96 | label.shadowOffset = CGSize(width: 0.0, height: 1.0) 97 | label.font = SKToolbarOptions.font 98 | label.textColor = SKToolbarOptions.textColor 99 | label.translatesAutoresizingMaskIntoConstraints = true 100 | label.autoresizingMask = [.flexibleBottomMargin, 101 | .flexibleLeftMargin, 102 | .flexibleRightMargin, 103 | .flexibleTopMargin] 104 | addSubview(label) 105 | counterLabel = label 106 | } 107 | 108 | func setupPrevButton() { 109 | guard SKPhotoBrowserOptions.displayBackAndForwardButton else { return } 110 | guard browser?.photos.count ?? 0 > 1 else { return } 111 | 112 | let button = SKPrevButton(frame: frame) 113 | button.center = CGPoint(x: frame.width / 2 - 100, y: frame.height / 2) 114 | button.addTarget(browser, action: #selector(SKPhotoBrowser.gotoPreviousPage), for: .touchUpInside) 115 | addSubview(button) 116 | prevButton = button 117 | } 118 | 119 | func setupNextButton() { 120 | guard SKPhotoBrowserOptions.displayBackAndForwardButton else { return } 121 | guard browser?.photos.count ?? 0 > 1 else { return } 122 | 123 | let button = SKNextButton(frame: frame) 124 | button.center = CGPoint(x: frame.width / 2 + 100, y: frame.height / 2) 125 | button.addTarget(browser, action: #selector(SKPhotoBrowser.gotoNextPage), for: .touchUpInside) 126 | addSubview(button) 127 | nextButton = button 128 | } 129 | } 130 | 131 | class SKPaginationButton: UIButton { 132 | let insets: UIEdgeInsets = UIEdgeInsets(top: 13.25, left: 17.25, bottom: 13.25, right: 17.25) 133 | 134 | func setup(_ imageName: String) { 135 | backgroundColor = .clear 136 | imageEdgeInsets = insets 137 | translatesAutoresizingMaskIntoConstraints = true 138 | autoresizingMask = [.flexibleBottomMargin, 139 | .flexibleLeftMargin, 140 | .flexibleRightMargin, 141 | .flexibleTopMargin] 142 | contentMode = .center 143 | 144 | setImage(UIImage.bundledImage(named: imageName), for: .normal) 145 | } 146 | } 147 | 148 | class SKPrevButton: SKPaginationButton { 149 | let imageName = "btn_common_back_wh" 150 | required init?(coder aDecoder: NSCoder) { 151 | super.init(coder: aDecoder) 152 | } 153 | 154 | override init(frame: CGRect) { 155 | super.init(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) 156 | setup(imageName) 157 | } 158 | } 159 | 160 | class SKNextButton: SKPaginationButton { 161 | let imageName = "btn_common_forward_wh" 162 | required init?(coder aDecoder: NSCoder) { 163 | super.init(coder: aDecoder) 164 | } 165 | 166 | override init(frame: CGRect) { 167 | super.init(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) 168 | setup(imageName) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/FromCameraRollViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraRollCollectionViewController.swift 3 | // SKPhotoBrowserExample 4 | // 5 | // Created by K Rummler on 11/03/16. 6 | // Copyright © 2016 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | import SKPhotoBrowser 12 | 13 | class FromCameraRollViewController: UIViewController, SKPhotoBrowserDelegate, UICollectionViewDataSource, UICollectionViewDelegate { 14 | @IBOutlet weak var collectionView: UICollectionView! 15 | fileprivate let imageManager = PHCachingImageManager.default() 16 | fileprivate var assets: [PHAsset] = [] 17 | 18 | fileprivate lazy var requestOptions: PHImageRequestOptions = { 19 | let options = PHImageRequestOptions() 20 | options.deliveryMode = .opportunistic 21 | options.resizeMode = .fast 22 | 23 | return options 24 | }() 25 | 26 | fileprivate lazy var bigRequestOptions: PHImageRequestOptions = { 27 | let options = PHImageRequestOptions() 28 | options.deliveryMode = .highQualityFormat 29 | options.resizeMode = .fast 30 | 31 | return options 32 | }() 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | fetchAssets() 38 | collectionView?.reloadData() 39 | } 40 | 41 | override func didReceiveMemoryWarning() { 42 | super.didReceiveMemoryWarning() 43 | // Dispose of any resources that can be recreated. 44 | } 45 | 46 | /* 47 | // MARK: - Navigation 48 | 49 | // In a storyboard-based application, you will often want to do a little preparation before navigation 50 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 51 | // Get the new view controller using [segue destinationViewController]. 52 | // Pass the selected object to the new view controller. 53 | } 54 | */ 55 | 56 | // MARK: UICollectionViewDataSource 57 | func numberOfSections(in collectionView: UICollectionView) -> Int { 58 | // #warning Incomplete implementation, return the number of sections 59 | return 1 60 | } 61 | 62 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 63 | // #warning Incomplete implementation, return the number of items 64 | return assets.count 65 | } 66 | 67 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 68 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "exampleCollectionViewCell", for: indexPath) 69 | let asset = assets[(indexPath as NSIndexPath).row] 70 | 71 | if let cell = cell as? AssetExampleCollectionViewCell { 72 | if let id = cell.requestId { 73 | imageManager.cancelImageRequest(id) 74 | cell.requestId = nil 75 | } 76 | 77 | cell.requestId = requestImageForAsset(asset, options: requestOptions) { image, requestId in 78 | if requestId == cell.requestId || cell.requestId == nil { 79 | 80 | cell.exampleImageView.image = image 81 | } 82 | } 83 | } 84 | 85 | return cell 86 | } 87 | 88 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 89 | guard let cell = collectionView.cellForItem(at: indexPath) as? ExampleCollectionViewCell else { 90 | return 91 | } 92 | guard let originImage = cell.exampleImageView.image else { 93 | return 94 | } 95 | 96 | func open(_ images: [UIImage]) { 97 | let photoImages: [SKPhotoProtocol] = images.map({ return SKPhoto.photoWithImage($0) }) 98 | let browser = SKPhotoBrowser(originImage: cell.exampleImageView.image!, photos: photoImages, animatedFromView: cell) 99 | browser.initializePageIndex(indexPath.row) 100 | browser.delegate = self 101 | // browser.displayDeleteButton = true 102 | // browser.displayAction = false 103 | self.present(browser, animated: true, completion: {}) 104 | } 105 | 106 | var fetchedImages: [UIImage] = [UIImage](repeating: UIImage(), count: assets.count) 107 | var fetched = 0 108 | 109 | assets.forEach { (asset) -> Void in 110 | 111 | _ = requestImageForAsset(asset, options: bigRequestOptions, completion: { [weak self] (image, _) -> Void in 112 | 113 | if let image = image, let index = self?.assets.firstIndex(of: asset) { 114 | fetchedImages[index] = image 115 | } 116 | fetched += 1 117 | 118 | if self?.assets.count == fetched { 119 | open(fetchedImages) 120 | } 121 | }) 122 | } 123 | } 124 | 125 | fileprivate func fetchAssets() { 126 | 127 | let options = PHFetchOptions() 128 | let limit = 8 129 | 130 | options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 131 | options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue) 132 | 133 | options.fetchLimit = limit 134 | 135 | let result = PHAsset.fetchAssets(with: options) 136 | let amount = min(result.count, limit) 137 | self.assets = result.objects(at: IndexSet(integersIn: Range(NSRange(location: 0, length: amount)) ?? 0..<0)) 138 | } 139 | 140 | fileprivate func requestImageForAsset(_ asset: PHAsset, options: PHImageRequestOptions, completion: @escaping (_ image: UIImage?, _ requestId: PHImageRequestID?) -> Void) -> PHImageRequestID { 141 | 142 | let scale = UIScreen.main.scale 143 | let targetSize: CGSize 144 | 145 | if options.deliveryMode == .highQualityFormat { 146 | targetSize = CGSize(width: 600 * scale, height: 600 * scale) 147 | } else { 148 | targetSize = CGSize(width: 182 * scale, height: 182 * scale) 149 | } 150 | 151 | requestOptions.isSynchronous = false 152 | 153 | // Workaround because PHImageManager.requestImageForAsset doesn't work for burst images 154 | if asset.representsBurst { 155 | return imageManager.requestImageData(for: asset, options: options) { data, _, _, dict in 156 | let image = data.flatMap { UIImage(data: $0) } 157 | let requestId = dict?[PHImageResultRequestIDKey] as? NSNumber 158 | completion(image, requestId?.int32Value) 159 | } 160 | } else { 161 | return imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { image, dict in 162 | let requestId = dict?[PHImageResultRequestIDKey] as? NSNumber 163 | completion(image, requestId?.int32Value) 164 | } 165 | } 166 | } 167 | 168 | override var prefersStatusBarHidden: Bool { 169 | return false 170 | } 171 | 172 | override var preferredStatusBarStyle: UIStatusBarStyle { 173 | return .lightContent 174 | } 175 | } 176 | 177 | class AssetExampleCollectionViewCell: ExampleCollectionViewCell { 178 | var requestId: PHImageRequestID? 179 | } 180 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/FromLocalViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SKPhotoBrowserExample 4 | // 5 | // Created by suzuki_keishi on 2015/10/06. 6 | // Copyright © 2015 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SKPhotoBrowser 11 | 12 | class FromLocalViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, SKPhotoBrowserDelegate { 13 | @IBOutlet weak var collectionView: UICollectionView! 14 | 15 | var images = [SKPhotoProtocol]() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | // Static setup 21 | SKPhotoBrowserOptions.displayAction = true 22 | SKPhotoBrowserOptions.displayStatusbar = true 23 | SKPhotoBrowserOptions.displayCounterLabel = true 24 | SKPhotoBrowserOptions.displayBackAndForwardButton = true 25 | 26 | setupTestData() 27 | setupCollectionView() 28 | } 29 | 30 | override var prefersStatusBarHidden: Bool { 31 | return false 32 | } 33 | 34 | override var preferredStatusBarStyle: UIStatusBarStyle { 35 | return .lightContent 36 | } 37 | } 38 | 39 | // MARK: - UICollectionViewDataSource 40 | extension FromLocalViewController { 41 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 42 | return images.count 43 | } 44 | 45 | @objc(collectionView:cellForItemAtIndexPath:) func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 46 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "exampleCollectionViewCell", for: indexPath) as? ExampleCollectionViewCell else { 47 | return UICollectionViewCell() 48 | } 49 | 50 | cell.exampleImageView.image = UIImage(named: "image\((indexPath as NSIndexPath).row % 10).jpg") 51 | return cell 52 | } 53 | } 54 | 55 | // MARK: - UICollectionViewDelegate 56 | 57 | extension FromLocalViewController { 58 | @objc(collectionView:didSelectItemAtIndexPath:) func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 59 | let browser = SKPhotoBrowser(photos: images, initialPageIndex: indexPath.row) 60 | browser.delegate = self 61 | 62 | present(browser, animated: true, completion: {}) 63 | } 64 | 65 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize { 66 | if UIDevice.current.userInterfaceIdiom == .pad { 67 | return CGSize(width: UIScreen.main.bounds.size.width / 2 - 5, height: 300) 68 | } else { 69 | return CGSize(width: UIScreen.main.bounds.size.width / 2 - 5, height: 200) 70 | } 71 | } 72 | } 73 | 74 | // MARK: - SKPhotoBrowserDelegate 75 | 76 | extension FromLocalViewController { 77 | func didShowPhotoAtIndex(_ index: Int) { 78 | collectionView.visibleCells.forEach({$0.isHidden = false}) 79 | collectionView.cellForItem(at: IndexPath(item: index, section: 0))?.isHidden = true 80 | } 81 | 82 | func willDismissAtPageIndex(_ index: Int) { 83 | collectionView.visibleCells.forEach({$0.isHidden = false}) 84 | collectionView.cellForItem(at: IndexPath(item: index, section: 0))?.isHidden = true 85 | } 86 | 87 | func willShowActionSheet(_ photoIndex: Int) { 88 | // do some handle if you need 89 | } 90 | 91 | func didDismissAtPageIndex(_ index: Int) { 92 | collectionView.cellForItem(at: IndexPath(item: index, section: 0))?.isHidden = false 93 | } 94 | 95 | func didDismissActionSheetWithButtonIndex(_ buttonIndex: Int, photoIndex: Int) { 96 | // handle dismissing custom actions 97 | } 98 | 99 | func removePhoto(_ browser: SKPhotoBrowser, index: Int, reload: @escaping (() -> Void)) { 100 | reload() 101 | } 102 | 103 | func viewForPhoto(_ browser: SKPhotoBrowser, index: Int) -> UIView? { 104 | return collectionView.cellForItem(at: IndexPath(item: index, section: 0)) 105 | } 106 | 107 | func captionViewForPhotoAtIndex(index: Int) -> SKCaptionView? { 108 | return nil 109 | } 110 | } 111 | 112 | // MARK: - private 113 | 114 | private extension FromLocalViewController { 115 | func setupTestData() { 116 | images = createLocalPhotos() 117 | } 118 | 119 | func setupCollectionView() { 120 | collectionView.delegate = self 121 | collectionView.dataSource = self 122 | } 123 | 124 | func createLocalPhotos() -> [SKPhotoProtocol] { 125 | return (0..<10).map { (i: Int) -> SKPhotoProtocol in 126 | let photo = SKPhoto.photoWithImage(UIImage(named: "image\(i%10).jpg")!) 127 | photo.caption = caption[i%10] 128 | return photo 129 | } 130 | } 131 | } 132 | 133 | class ExampleCollectionViewCell: UICollectionViewCell { 134 | @IBOutlet weak var exampleImageView: UIImageView! 135 | 136 | override func awakeFromNib() { 137 | super.awakeFromNib() 138 | exampleImageView.image = nil 139 | // layer.cornerRadius = 25.0 140 | layer.masksToBounds = true 141 | } 142 | 143 | override func prepareForReuse() { 144 | exampleImageView.image = nil 145 | } 146 | } 147 | 148 | var caption = ["Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 149 | "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book", 150 | "It has survived not only five centuries, but also the leap into electronic typesetting", 151 | "remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", 152 | "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 153 | "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", 154 | "It has survived not only five centuries, but also the leap into electronic typesetting", 155 | "remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", 156 | "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 157 | "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", 158 | "It has survived not only five centuries, but also the leap into electronic typesetting", 159 | "remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." 160 | ] 161 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKAnimator.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by keishi suzuki on 2016/08/09. 6 | // Copyright © 2016 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol SKPhotoBrowserAnimatorDelegate { 12 | func willPresent(_ browser: SKPhotoBrowser) 13 | func willDismiss(_ browser: SKPhotoBrowser) 14 | } 15 | 16 | class SKAnimator: NSObject, SKPhotoBrowserAnimatorDelegate { 17 | fileprivate let window = UIApplication.shared.preferredApplicationWindow 18 | fileprivate var resizableImageView: UIImageView? 19 | fileprivate var finalImageViewFrame: CGRect = .zero 20 | 21 | internal lazy var backgroundView: UIView = { 22 | guard let window = UIApplication.shared.preferredApplicationWindow else { fatalError() } 23 | 24 | let backgroundView = UIView(frame: window.frame) 25 | backgroundView.backgroundColor = SKPhotoBrowserOptions.backgroundColor 26 | backgroundView.alpha = 0.0 27 | return backgroundView 28 | }() 29 | internal var senderOriginImage: UIImage! 30 | internal var senderViewOriginalFrame: CGRect = .zero 31 | internal var senderViewForAnimation: UIView? 32 | 33 | fileprivate var animationDuration: TimeInterval { 34 | if SKPhotoBrowserOptions.bounceAnimation { return 0.5 } 35 | return 0.35 36 | } 37 | fileprivate var animationDamping: CGFloat { 38 | if SKPhotoBrowserOptions.bounceAnimation { return 0.8 } 39 | return 1.0 40 | } 41 | 42 | override init() { 43 | super.init() 44 | window?.addSubview(backgroundView) 45 | } 46 | 47 | deinit { 48 | backgroundView.removeFromSuperview() 49 | } 50 | 51 | func willPresent(_ browser: SKPhotoBrowser) { 52 | guard let sender = browser.delegate?.viewForPhoto?(browser, index: browser.currentPageIndex) ?? senderViewForAnimation else { 53 | presentAnimation(browser) 54 | return 55 | } 56 | 57 | let photo = browser.photoAtIndex(browser.currentPageIndex) 58 | let imageFromView = (senderOriginImage ?? browser.getImageFromView(sender)).rotateImageByOrientation() 59 | let imageRatio = imageFromView.size.width / imageFromView.size.height 60 | 61 | senderViewOriginalFrame = calcOriginFrame(sender) 62 | finalImageViewFrame = calcFinalFrame(imageRatio) 63 | resizableImageView = UIImageView(image: imageFromView) 64 | 65 | if let resizableImageView = resizableImageView { 66 | resizableImageView.frame = senderViewOriginalFrame 67 | resizableImageView.clipsToBounds = true 68 | resizableImageView.contentMode = photo.contentMode 69 | if sender.layer.cornerRadius != 0 { 70 | let duration = (animationDuration * Double(animationDamping)) 71 | resizableImageView.layer.masksToBounds = true 72 | resizableImageView.addCornerRadiusAnimation(sender.layer.cornerRadius, to: 0, duration: duration) 73 | } 74 | window?.addSubview(resizableImageView) 75 | } 76 | 77 | presentAnimation(browser) 78 | } 79 | 80 | func willDismiss(_ browser: SKPhotoBrowser) { 81 | guard let sender = browser.delegate?.viewForPhoto?(browser, index: browser.currentPageIndex), 82 | let image = browser.photoAtIndex(browser.currentPageIndex).underlyingImage, 83 | let scrollView = browser.pageDisplayedAtIndex(browser.currentPageIndex) else { 84 | 85 | senderViewForAnimation?.isHidden = false 86 | browser.dismissPhotoBrowser(animated: false) { 87 | self.resizableImageView?.removeFromSuperview() 88 | self.backgroundView.removeFromSuperview() 89 | } 90 | return 91 | } 92 | 93 | senderViewForAnimation = sender 94 | browser.view.isHidden = true 95 | backgroundView.isHidden = false 96 | backgroundView.alpha = 1.0 97 | backgroundView.backgroundColor = .clear 98 | senderViewOriginalFrame = calcOriginFrame(sender) 99 | 100 | if let resizableImageView = resizableImageView { 101 | let photo = browser.photoAtIndex(browser.currentPageIndex) 102 | let contentOffset = scrollView.contentOffset 103 | let scrollFrame = scrollView.imageView.frame 104 | let offsetY = scrollView.center.y - (scrollView.bounds.height/2) 105 | let frame = CGRect( 106 | x: scrollFrame.origin.x - contentOffset.x, 107 | y: scrollFrame.origin.y + contentOffset.y + offsetY - scrollView.contentOffset.y, 108 | width: scrollFrame.width, 109 | height: scrollFrame.height) 110 | 111 | resizableImageView.image = image.rotateImageByOrientation() 112 | resizableImageView.frame = frame 113 | resizableImageView.alpha = 1.0 114 | resizableImageView.clipsToBounds = true 115 | resizableImageView.contentMode = photo.contentMode 116 | if let view = senderViewForAnimation, view.layer.cornerRadius != 0 { 117 | let duration = (animationDuration * Double(animationDamping)) 118 | resizableImageView.layer.masksToBounds = true 119 | resizableImageView.addCornerRadiusAnimation(0, to: view.layer.cornerRadius, duration: duration) 120 | } 121 | } 122 | dismissAnimation(browser) 123 | } 124 | } 125 | 126 | private extension SKAnimator { 127 | func calcOriginFrame(_ sender: UIView) -> CGRect { 128 | if let senderViewOriginalFrameTemp = sender.superview?.convert(sender.frame, to: nil) { 129 | return senderViewOriginalFrameTemp 130 | } else if let senderViewOriginalFrameTemp = sender.layer.superlayer?.convert(sender.frame, to: nil) { 131 | return senderViewOriginalFrameTemp 132 | } else { 133 | return .zero 134 | } 135 | } 136 | 137 | func calcFinalFrame(_ imageRatio: CGFloat) -> CGRect { 138 | guard !imageRatio.isNaN else { return .zero } 139 | 140 | if SKMesurement.screenRatio < imageRatio { 141 | let width = SKMesurement.screenWidth 142 | let height = width / imageRatio 143 | let yOffset = (SKMesurement.screenHeight - height) / 2 144 | return CGRect(x: 0, y: yOffset, width: width, height: height) 145 | 146 | } else if SKPhotoBrowserOptions.longPhotoWidthMatchScreen && imageRatio <= 1.0 { 147 | let height = SKMesurement.screenWidth / imageRatio 148 | return CGRect(x: 0.0, y: 0, width: SKMesurement.screenWidth, height: height) 149 | 150 | } else { 151 | let height = SKMesurement.screenHeight 152 | let width = height * imageRatio 153 | let xOffset = (SKMesurement.screenWidth - width) / 2 154 | return CGRect(x: xOffset, y: 0, width: width, height: height) 155 | } 156 | } 157 | } 158 | 159 | private extension SKAnimator { 160 | func presentAnimation(_ browser: SKPhotoBrowser, completion: (() -> Void)? = nil) { 161 | let finalFrame = self.finalImageViewFrame 162 | browser.view.isHidden = true 163 | browser.view.alpha = 0.0 164 | 165 | if #available(iOS 11.0, *) { 166 | backgroundView.accessibilityIgnoresInvertColors = true 167 | self.resizableImageView?.accessibilityIgnoresInvertColors = true 168 | } 169 | 170 | UIView.animate( 171 | withDuration: animationDuration, 172 | delay: 0, 173 | usingSpringWithDamping: animationDamping, 174 | initialSpringVelocity: 0, 175 | options: UIView.AnimationOptions(), 176 | animations: { 177 | browser.showButtons() 178 | self.backgroundView.alpha = 1.0 179 | self.resizableImageView?.frame = finalFrame 180 | }, 181 | completion: { (_) -> Void in 182 | browser.view.alpha = 1.0 183 | browser.view.isHidden = false 184 | self.backgroundView.isHidden = true 185 | self.resizableImageView?.alpha = 0.0 186 | }) 187 | } 188 | 189 | func dismissAnimation(_ browser: SKPhotoBrowser, completion: (() -> Void)? = nil) { 190 | let finalFrame = self.senderViewOriginalFrame 191 | 192 | UIView.animate( 193 | withDuration: animationDuration, 194 | delay: 0, 195 | usingSpringWithDamping: animationDamping, 196 | initialSpringVelocity: 0, 197 | options: UIView.AnimationOptions(), 198 | animations: { 199 | self.backgroundView.alpha = 0.0 200 | self.resizableImageView?.layer.frame = finalFrame 201 | }, 202 | completion: { (_) -> Void in 203 | browser.dismissPhotoBrowser(animated: true) { 204 | self.resizableImageView?.removeFromSuperview() 205 | self.backgroundView.removeFromSuperview() 206 | } 207 | }) 208 | } 209 | } 210 | 211 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 7.0.0 4 | ### Big Changed 5 | - Support xcode12.x. swift5.2 6 | 7 | #### Updated 8 | - #371 Update README.md by bcorrea2 9 | - #374 Support network gif loading by p36348 10 | 11 | ## 6.1.0 12 | 13 | ### Big Changed 14 | - #330 Changes for swift 5.0, Xcode 10.2, 15 | 16 | #### Updated 17 | - #339 fix #316 and #323 bug by xiaoweiFive 18 | - #342 update readme.md by SherlockQi 19 | - #344 fixed broken page recycling (regression from c6df44f9) 20 | - #348 keep thumbnail display until image download finished by seanxux 21 | - #353 loop call SKPhotoBrowserDelegate.controlsVisibilityToggled function by cp110 22 | - #361 Support for iPhoneXS, XSMax and XR by jdanthinne 23 | - #361 Xcode 10.2 optimization by TParizek 24 | 25 | ## 6.0.0 26 | 27 | ### Big Changed 28 | - #330 Changes for swift 4.2, Xcode 10, and iOS 12 by jlcanale 29 | 30 | #### Updated 31 | - #314 Add possibility to provide custom request parameters by Fiser33 32 | - #315 Fix: Unable to set delete and close button images without setting size. by kiztonwose 33 | - #318 fix unreleased views in uiwindow for non-dismiss animation case by fans3210 34 | - #321 Set the backround view's background color from settings by pantelisss 35 | - #331 Add ability to lower caption and give caption a background gradient by corban123 36 | - #334 Prevent app crashed on zooming when xScale or yScale is NaN, Inf by GE-N 37 | - #335 use the size of the window instead of UIScreen to support SplitScreen by PatrickDotStar 38 | 39 | ## 5.1.0 40 | 41 | #### Updated 42 | - #311 Delete and Close Button Overlapping bug by rajendersha 43 | 44 | ## 5.0.9 45 | 46 | #### Updated 47 | - #304 CaptionViewForPhotoAtIndex is not work 48 | - #305 Padding properties for close and delete button. 49 | - Bug At iphoneX, close / delete / pagination can be tapped correctly. 50 | 51 | ## 5.0.8 52 | 53 | #### Updated 54 | - #224 override popupShare not working 55 | - #248 always ignore image cache, asked by FicowShen 56 | - #304 CaptionViewForPhotoAtIndex is not work 57 | - #301 SKPhotoBrowserOptions.displayDeleteButton not working 58 | - #302 Add method to remove all images for SKCache by filograno 59 | 60 | ## 5.0.7 61 | 62 | #### Updated 63 | - #301 SKPhotoBrowserOptions.displayCounterLabel is not working 64 | - #297 I want to hide SKPagingScrollView's horizontal indicator by mothule 65 | 66 | ## 5.0.6 67 | 68 | #### Updated 69 | - #292 Fix crash when imageRatio isNan by arnaudWasappli 70 | - #291 When disableVerticalSwipe is true the browser crashes on close by aliillyas 71 | 72 | ## 5.0.5 73 | 74 | #### Updated 75 | - #271 SmartInvert now works properly by timroesner 76 | - #288 Add the long photo width match screen option by dirtmelon 77 | - #289 Add SWIFT_VERSION to xcconfig by cheungpat 78 | 79 | ## 5.0.4 80 | 81 | #### Updated 82 | - #273 Fixed crash on resizableImageView force unwrapping by matuslittva 83 | 84 | ## 5.0.3 85 | 86 | #### Updated 87 | - Refactoring for swift4.0 88 | 89 | ## 5.0.2 90 | 91 | #### Updated 92 | - #255 Fixed the crash where the PhotoBrowser could crash. 93 | - #262 Fix calling willDismissAtPageIndex delegate method 94 | - #263 Remove unused options 95 | - #263 Use iOS 11 Safe Area Insets to layout toolbar 96 | - #270 Added functionality to add new photos at the end or at the start of c… 97 | 98 | ## 5.0.1 99 | 100 | #### Updated 101 | - #246 Updated to Swift 4 and made Swift Lint recommended changes 102 | 103 | ## 5.0.0 104 | 105 | #### Major changed 106 | - #250 Swift4 merge 107 | - #242 swift4 merge 108 | 109 | #### Updated 110 | - #239 Updated padding for iPhone X 111 | 112 | ## 4.1.1 113 | 114 | #### Updated 115 | - #208 improve: change deleteButtonPressed(), currentPageIndex access level 116 | - #210 Fix Shorthand Operator Violation of Swiftlint 117 | - #215 swiftLint 118 | - #216 update code to Swift 3.1 119 | - #223 Removed deprecated constants 120 | - #225 Custom Cancel button title 121 | - #227 Attach toolbar and delete button to single browser instance 122 | - #236 improve SKPhotoBrowserDelegate 123 | 124 | ## 4.1.0 125 | Released on 30-8-2017 126 | 127 | #### Updated 128 | - #173 Move the willDismiss delegate call closer to the dismissal 129 | - #196 Improved SKCaptionView 130 | - #197 fix: deleteButton frame does not update if screen has rotated 131 | - #199 Add SKPhotoBrowserOptions to customize indicator color & style 132 | - #200 Swap and custom padding for delete and close buttons 133 | - #205 Replaced deprecated Pi constants 134 | - #207 Update code style: to Swift3.1 135 | - #231 Update SKZoomingScrollView.swift 136 | 137 | ## 4.0.1 138 | Released on 18-1-2017 139 | 140 | #### Fixed 141 | - Update README.md 142 | - #158 Button Position wrong with changed StatusBar handling 143 | - #162 Fix SKPhotoBrowserOptions background color 144 | - #181 Unclear how placeholder image is supposed to work 145 | 146 | ## 4.0.0 147 | Released on 5-1-2017 148 | 149 | #### Breaking Change 150 | - default swift version change. swift2.2 -> swift3 151 | 152 | #### Fixed 153 | - #171 Add @escaping to delegate method's reload closure parameter. 154 | - #172 Fix caption font bug 155 | - #177 Fix a fatal error when app's window is customized. 156 | - #178 SKPagingScrollView fixes / swift3 branch 157 | - #179 SKPagingScrollView fixes 158 | - #182 Always load from the URL even if a placeholder image exists 159 | - #186 fix setStatusBarHidden is deprecated in iOS 9.0 and demo cannot run 160 | - #188 Added options for custom photo's caption. 161 | - #180 SKPhotoBrowserOptions not working Swift 3 162 | 163 | ## 3.1.4 164 | Released on 11-14-2016 165 | - add delegate that get notified when controls view visibility toggled 166 | 167 | ## 3.1.3 168 | Released on 23-9-2016 169 | 170 | #### Fixed 171 | - The method dismissPhotoBrowser should only animate if the parameter animated is true. 172 | 173 | ## 3.1.2 174 | 175 | Released on 16-9-2016 176 | 177 | #### Fixed 178 | - Scrolling performance slowed #145 179 | 180 | ## 3.1.1 181 | 182 | Released on 15-9-2016 183 | 184 | #### Fixed 185 | - Example crash in xcode8 fixed 186 | - Provides various UI configuration options via SKPhotoBrowserOptions. #144 187 | 188 | ## 3.1.0 189 | 190 | Released on 9-2016 191 | 192 | #### Fixed 193 | - Issue with multiple actionButtonTitles #137 194 | - fix swiftlint warnings #140 195 | - Update for Xcode 8 GM (swift 2.3). #141 196 | 197 | ## 3.0.2 198 | 199 | Released on 9-2016 200 | 201 | #### Fixed 202 | - Issue with multiple actionButtonTitles #137 203 | - Impossible to zoom when resolution is 1024x768 #134 204 | - Crash bug at zooming scrool view #133 205 | 206 | ## 3.0.1 207 | 208 | Released on 9-2016 209 | 210 | #### Fixed 211 | - Skip loading image if already loaded #135 212 | 213 | Released on 8-2016 214 | 215 | #### Some Interface is removed, changed this version. 216 | - status bar handling is removed. 217 | - custom button handling interface is chagned. 218 | - custom option goes internal/private. use option via SKPhotoBrowserOptions. 219 | 220 | #### Add 221 | - Add changelog 222 | 223 | #### Fixed 224 | - prepare for swift3.0. 225 | - refactoring code for new implement. 226 | - Parent View disappears when dismissed. #120 227 | - Glitch when origin imageview is not correct size #108 228 | - Problems with the "long" photo #116 229 | 230 | #### Remove 231 | - Statusbar handling. 232 | - Some public property to internal for improving 233 | 234 | ## 2.0.x 235 | Released on 8-2016 236 | 237 | #### Added 238 | - Migrate UIImage cache category to new SKCache 239 | 240 | #### Fixed 241 | - Make cached response data return optional 242 | - Fixed issue when animatedFromView not has a superview but has superlayer 243 | - Fixed when image downloaded then not show activityindicator 244 | - Update for Swift2.3 245 | 246 | --- 247 | 248 | ## 1.9.x 249 | Released on 6-2016 250 | 251 | #### Added 252 | - Delegate to notify when the user scroll to an index 253 | - Single tap to dismiss 254 | 255 | #### Fixed 256 | - Fixed a bug where the activity indicator was only visible 257 | - Fixed unit test and problems running when being bridged 258 | 259 | --- 260 | 261 | ## 1.8.x 262 | Released on 4-2016 263 | 264 | #### Added 265 | - Using SKPhotoProtocol to enable usage from SKLocalPhoto 266 | - SKLocalPhoto to support local photo from file 267 | 268 | #### Fixed 269 | - Bug when animation when tap. 270 | - The indicator may not disappear when loading local image 271 | - Event crash when closing before image has been loaded 272 | - Fix crash on initialisation 273 | 274 | --- 275 | 276 | ## 1.7.x 277 | Released on 3-2016 278 | 279 | #### Added 280 | - Enable ability to override statusBar style 281 | 282 | #### Fixed 283 | - Update for swift2.0 284 | - Bug when zooming small image 285 | - Prevent crash when closing before image has been loaded 286 | 287 | --- 288 | 289 | ## 1.6.x 290 | Released on 2016-3 291 | 292 | #### Fixed 293 | - Change maxScale to 1.0 it works perfectly. 294 | - Fixed the bug which was after the device rotation 295 | 296 | --- 297 | 298 | ## 1.5.x 299 | Released on 2016-3 300 | 301 | #### Added 302 | - Delete Button 303 | 304 | #### Fixed 305 | - Change maxScale to 1.0 it works perfectly. 306 | - Rew algorithm for maxScale. 307 | - Changed UIActionSheet to UIAlertController with ActionSheet style 308 | 309 | --- 310 | 311 | ## 1.4.x 312 | Released on 2-2016 313 | 314 | #### Added 315 | - Delegate add for actionbutton. 316 | - DidShowPhotoAtIndex delegate goes to optional. 317 | 318 | #### Fixed 319 | - Zooming bug fixed. 320 | 321 | --- 322 | 323 | ## 1.3.x 324 | Released on 1-2016 325 | 326 | #### Added 327 | - Added action functionality similar to IDMPhotoBrowser. 328 | - Add extra caption for share 329 | 330 | #### Fixed 331 | - Bug fixed for mail crash 332 | 333 | 334 | --- 335 | 336 | ## 1.2.x 337 | Released on 10-2015 338 | 339 | #### Added 340 | - SKPhotoProtocol is implemented. 341 | 342 | #### Fixed 343 | - Double tap bug fixed 344 | 345 | --- 346 | 347 | ## 1.1.x 348 | Released on 10-2015 349 | 350 | #### Fixed 351 | - some property make private. 352 | - layout bug fixed when zoom. 353 | 354 | ## 1.0.0 355 | Released on 10-2015 356 | 357 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKPagingScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKPagingScrollView.swift 3 | // SKPhotoBrowser 4 | // 5 | // Created by 鈴木 啓司 on 2016/08/18. 6 | // Copyright © 2016年 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SKPagingScrollView: UIScrollView { 12 | fileprivate let pageIndexTagOffset: Int = 1000 13 | fileprivate let sideMargin: CGFloat = 10 14 | fileprivate var visiblePages: [SKZoomingScrollView] = [] 15 | fileprivate var recycledPages: [SKZoomingScrollView] = [] 16 | fileprivate weak var browser: SKPhotoBrowser? 17 | 18 | var numberOfPhotos: Int { 19 | return browser?.photos.count ?? 0 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) { 23 | super.init(coder: aDecoder) 24 | } 25 | 26 | override init(frame: CGRect) { 27 | super.init(frame: frame) 28 | } 29 | 30 | convenience init(frame: CGRect, browser: SKPhotoBrowser) { 31 | self.init(frame: frame) 32 | self.browser = browser 33 | 34 | isPagingEnabled = true 35 | showsHorizontalScrollIndicator = SKPhotoBrowserOptions.displayPagingHorizontalScrollIndicator 36 | showsVerticalScrollIndicator = true 37 | 38 | updateFrame(bounds, currentPageIndex: browser.currentPageIndex) 39 | } 40 | 41 | func reload() { 42 | visiblePages.forEach({$0.removeFromSuperview()}) 43 | visiblePages.removeAll() 44 | recycledPages.removeAll() 45 | } 46 | 47 | func loadAdjacentPhotosIfNecessary(_ photo: SKPhotoProtocol, currentPageIndex: Int) { 48 | guard let browser = browser, let page = pageDisplayingAtPhoto(photo) else { 49 | return 50 | } 51 | let pageIndex = (page.tag - pageIndexTagOffset) 52 | if currentPageIndex == pageIndex { 53 | // Previous 54 | if pageIndex > 0 { 55 | let previousPhoto = browser.photos[pageIndex - 1] 56 | if previousPhoto.underlyingImage == nil { 57 | previousPhoto.loadUnderlyingImageAndNotify() 58 | } 59 | } 60 | // Next 61 | if pageIndex < numberOfPhotos - 1 { 62 | let nextPhoto = browser.photos[pageIndex + 1] 63 | if nextPhoto.underlyingImage == nil { 64 | nextPhoto.loadUnderlyingImageAndNotify() 65 | } 66 | } 67 | } 68 | } 69 | 70 | func deleteImage() { 71 | // index equals 0 because when we slide between photos delete button is hidden and user cannot to touch on delete button. And visible pages number equals 0 72 | if numberOfPhotos > 0 { 73 | visiblePages[0].captionView?.removeFromSuperview() 74 | } 75 | } 76 | 77 | func jumpToPageAtIndex(_ frame: CGRect) { 78 | let point = CGPoint(x: frame.origin.x - sideMargin, y: 0) 79 | setContentOffset(point, animated: true) 80 | } 81 | 82 | func updateFrame(_ bounds: CGRect, currentPageIndex: Int) { 83 | var frame = bounds 84 | frame.origin.x -= sideMargin 85 | frame.size.width += (2 * sideMargin) 86 | 87 | self.frame = frame 88 | 89 | if visiblePages.count > 0 { 90 | for page in visiblePages { 91 | let pageIndex = page.tag - pageIndexTagOffset 92 | page.frame = frameForPageAtIndex(pageIndex) 93 | page.setMaxMinZoomScalesForCurrentBounds() 94 | if page.captionView != nil { 95 | page.captionView.frame = frameForCaptionView(page.captionView, index: pageIndex) 96 | } 97 | } 98 | } 99 | 100 | updateContentSize() 101 | updateContentOffset(currentPageIndex) 102 | } 103 | 104 | func updateContentSize() { 105 | contentSize = CGSize(width: bounds.size.width * CGFloat(numberOfPhotos), height: bounds.size.height) 106 | } 107 | 108 | func updateContentOffset(_ index: Int) { 109 | let pageWidth = bounds.size.width 110 | let newOffset = CGFloat(index) * pageWidth 111 | contentOffset = CGPoint(x: newOffset, y: 0) 112 | } 113 | 114 | func tilePages() { 115 | guard let browser = browser else { return } 116 | 117 | let firstIndex: Int = getFirstIndex() 118 | let lastIndex: Int = getLastIndex() 119 | 120 | visiblePages 121 | .filter({ $0.tag - pageIndexTagOffset < firstIndex || $0.tag - pageIndexTagOffset > lastIndex }) 122 | .forEach { page in 123 | recycledPages.append(page) 124 | page.prepareForReuse() 125 | page.removeFromSuperview() 126 | } 127 | 128 | let visibleSet: Set = Set(visiblePages) 129 | let visibleSetWithoutRecycled: Set = visibleSet.subtracting(recycledPages) 130 | visiblePages = Array(visibleSetWithoutRecycled) 131 | 132 | while recycledPages.count > 2 { 133 | recycledPages.removeFirst() 134 | } 135 | 136 | for index: Int in firstIndex...lastIndex { 137 | if visiblePages.filter({ $0.tag - pageIndexTagOffset == index }).count > 0 { 138 | continue 139 | } 140 | 141 | let page: SKZoomingScrollView = SKZoomingScrollView(frame: frame, browser: browser) 142 | page.frame = frameForPageAtIndex(index) 143 | page.tag = index + pageIndexTagOffset 144 | let photo = browser.photos[index] 145 | page.photo = photo 146 | if let thumbnail = browser.animator.senderOriginImage, 147 | index == browser.initPageIndex, 148 | photo.underlyingImage == nil { 149 | page.displayImage(thumbnail) 150 | } 151 | 152 | visiblePages.append(page) 153 | addSubview(page) 154 | 155 | // if exists caption, insert 156 | if let captionView: SKCaptionView = createCaptionView(index) { 157 | captionView.frame = frameForCaptionView(captionView, index: index) 158 | captionView.alpha = browser.areControlsHidden() ? 0 : 1 159 | addSubview(captionView) 160 | // ref val for control 161 | page.captionView = captionView 162 | } 163 | } 164 | } 165 | 166 | func frameForCaptionView(_ captionView: SKCaptionView, index: Int) -> CGRect { 167 | let pageFrame = frameForPageAtIndex(index) 168 | let captionSize = captionView.sizeThatFits(CGSize(width: pageFrame.size.width, height: 0)) 169 | let paginationFrame = browser?.paginationView.frame ?? .zero 170 | let toolbarFrame = browser?.toolbar.frame ?? .zero 171 | 172 | var frameSet = CGRect.zero 173 | switch SKCaptionOptions.captionLocation { 174 | case .basic: 175 | frameSet = paginationFrame 176 | case .bottom: 177 | frameSet = toolbarFrame 178 | } 179 | 180 | return CGRect(x: pageFrame.origin.x, 181 | y: pageFrame.size.height - captionSize.height - frameSet.height, 182 | width: pageFrame.size.width, height: captionSize.height) 183 | } 184 | 185 | func pageDisplayedAtIndex(_ index: Int) -> SKZoomingScrollView? { 186 | for page in visiblePages where page.tag - pageIndexTagOffset == index { 187 | return page 188 | } 189 | return nil 190 | } 191 | 192 | func pageDisplayingAtPhoto(_ photo: SKPhotoProtocol) -> SKZoomingScrollView? { 193 | for page in visiblePages where page.photo === photo { 194 | return page 195 | } 196 | return nil 197 | } 198 | 199 | func getCaptionViews() -> Set { 200 | var captionViews = Set() 201 | visiblePages 202 | .filter { $0.captionView != nil } 203 | .forEach { captionViews.insert($0.captionView) } 204 | return captionViews 205 | } 206 | 207 | func setControlsHidden(hidden: Bool) { 208 | let captionViews = getCaptionViews() 209 | let alpha: CGFloat = hidden ? 0.0 : 1.0 210 | 211 | UIView.animate(withDuration: 0.35, 212 | animations: { () -> Void in 213 | captionViews.forEach { $0.alpha = alpha } 214 | }, completion: nil) 215 | } 216 | } 217 | 218 | private extension SKPagingScrollView { 219 | func frameForPageAtIndex(_ index: Int) -> CGRect { 220 | var pageFrame = bounds 221 | pageFrame.size.width -= (2 * sideMargin) 222 | pageFrame.origin.x = (bounds.size.width * CGFloat(index)) + sideMargin 223 | return pageFrame 224 | } 225 | 226 | func createCaptionView(_ index: Int) -> SKCaptionView? { 227 | if let delegate = self.browser?.delegate, let ownCaptionView = delegate.captionViewForPhotoAtIndex?(index: index) { 228 | return ownCaptionView 229 | } 230 | guard let photo = browser?.photoAtIndex(index), photo.caption != nil else { 231 | return nil 232 | } 233 | return SKCaptionView(photo: photo) 234 | } 235 | 236 | func getFirstIndex() -> Int { 237 | let firstIndex = Int(floor((bounds.minX + sideMargin * 2) / bounds.width)) 238 | if firstIndex < 0 { 239 | return 0 240 | } 241 | if firstIndex > numberOfPhotos - 1 { 242 | return numberOfPhotos - 1 243 | } 244 | return firstIndex 245 | } 246 | 247 | func getLastIndex() -> Int { 248 | let lastIndex = Int(floor((bounds.maxX - sideMargin * 2 - 1) / bounds.width)) 249 | if lastIndex < 0 { 250 | return 0 251 | } 252 | if lastIndex > numberOfPhotos - 1 { 253 | return numberOfPhotos - 1 254 | } 255 | return lastIndex 256 | } 257 | } 258 | 259 | -------------------------------------------------------------------------------- /SKPhotoBrowser/SKZoomingScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKZoomingScrollView.swift 3 | // SKViewExample 4 | // 5 | // Created by suzuki_keihsi on 2015/10/01. 6 | // Copyright © 2015 suzuki_keishi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SKZoomingScrollView: UIScrollView { 12 | var captionView: SKCaptionView! 13 | var photo: SKPhotoProtocol! { 14 | didSet { 15 | imageView.image = nil 16 | if photo != nil && photo.underlyingImage != nil { 17 | displayImage(complete: true) 18 | return 19 | } 20 | if photo != nil { 21 | displayImage(complete: false) 22 | } 23 | } 24 | } 25 | 26 | fileprivate weak var browser: SKPhotoBrowser? 27 | 28 | fileprivate(set) var imageView: SKDetectingImageView! 29 | fileprivate var tapView: SKDetectingView! 30 | fileprivate var indicatorView: SKIndicatorView! 31 | 32 | required public init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | setup() 35 | } 36 | 37 | override init(frame: CGRect) { 38 | super.init(frame: frame) 39 | setup() 40 | } 41 | 42 | convenience init(frame: CGRect, browser: SKPhotoBrowser) { 43 | self.init(frame: frame) 44 | self.browser = browser 45 | setup() 46 | } 47 | 48 | deinit { 49 | browser = nil 50 | } 51 | 52 | func setup() { 53 | // tap 54 | tapView = SKDetectingView(frame: bounds) 55 | tapView.delegate = self 56 | tapView.backgroundColor = .clear 57 | tapView.autoresizingMask = [.flexibleHeight, .flexibleWidth] 58 | addSubview(tapView) 59 | 60 | // image 61 | imageView = SKDetectingImageView(frame: frame) 62 | imageView.delegate = self 63 | imageView.contentMode = .bottom 64 | imageView.backgroundColor = .clear 65 | addSubview(imageView) 66 | 67 | // indicator 68 | indicatorView = SKIndicatorView(frame: frame) 69 | addSubview(indicatorView) 70 | 71 | // self 72 | backgroundColor = .clear 73 | delegate = self 74 | showsHorizontalScrollIndicator = SKPhotoBrowserOptions.displayHorizontalScrollIndicator 75 | showsVerticalScrollIndicator = SKPhotoBrowserOptions.displayVerticalScrollIndicator 76 | autoresizingMask = [.flexibleWidth, .flexibleTopMargin, .flexibleBottomMargin, .flexibleRightMargin, .flexibleLeftMargin] 77 | } 78 | 79 | // MARK: - override 80 | 81 | open override func layoutSubviews() { 82 | tapView.frame = bounds 83 | indicatorView.frame = bounds 84 | 85 | super.layoutSubviews() 86 | 87 | let boundsSize = bounds.size 88 | var frameToCenter = imageView.frame 89 | 90 | // horizon 91 | if frameToCenter.size.width < boundsSize.width { 92 | frameToCenter.origin.x = floor((boundsSize.width - frameToCenter.size.width) / 2) 93 | } else { 94 | frameToCenter.origin.x = 0 95 | } 96 | // vertical 97 | if frameToCenter.size.height < boundsSize.height { 98 | frameToCenter.origin.y = floor((boundsSize.height - frameToCenter.size.height) / 2) 99 | } else { 100 | frameToCenter.origin.y = 0 101 | } 102 | 103 | // Center 104 | if !imageView.frame.equalTo(frameToCenter) { 105 | imageView.frame = frameToCenter 106 | } 107 | } 108 | 109 | open func setMaxMinZoomScalesForCurrentBounds() { 110 | maximumZoomScale = 1 111 | minimumZoomScale = 1 112 | zoomScale = 1 113 | 114 | guard let imageView = imageView else { 115 | return 116 | } 117 | 118 | let boundsSize = bounds.size 119 | let imageSize = imageView.frame.size 120 | 121 | let xScale = boundsSize.width / imageSize.width 122 | let yScale = boundsSize.height / imageSize.height 123 | var minScale: CGFloat = min(xScale.isNormal ? xScale : 1.0, yScale.isNormal ? yScale : 1.0) 124 | var maxScale: CGFloat = 1.0 125 | 126 | let scale = max(SKMesurement.screenScale, 2.0) 127 | let deviceScreenWidth = SKMesurement.screenWidth * scale // width in pixels. scale needs to remove if to use the old algorithm 128 | let deviceScreenHeight = SKMesurement.screenHeight * scale // height in pixels. scale needs to remove if to use the old algorithm 129 | 130 | if SKPhotoBrowserOptions.longPhotoWidthMatchScreen && imageView.frame.height >= imageView.frame.width { 131 | minScale = 1.0 132 | maxScale = 2.5 133 | } else if imageView.frame.width < deviceScreenWidth { 134 | // I think that we should to get coefficient between device screen width and image width and assign it to maxScale. I made two mode that we will get the same result for different device orientations. 135 | if UIApplication.shared.statusBarOrientation.isPortrait { 136 | maxScale = deviceScreenHeight / imageView.frame.width 137 | } else { 138 | maxScale = deviceScreenWidth / imageView.frame.width 139 | } 140 | } else if imageView.frame.width > deviceScreenWidth { 141 | maxScale = 1.0 142 | } else { 143 | // here if imageView.frame.width == deviceScreenWidth 144 | maxScale = 2.5 145 | } 146 | 147 | maximumZoomScale = maxScale 148 | minimumZoomScale = minScale 149 | zoomScale = minScale 150 | 151 | // on high resolution screens we have double the pixel density, so we will be seeing every pixel if we limit the 152 | // maximum zoom scale to 0.5 153 | // After changing this value, we still never use more 154 | maxScale /= scale 155 | if maxScale < minScale { 156 | maxScale = minScale * 2 157 | } 158 | 159 | // reset position 160 | imageView.frame.origin = CGPoint.zero 161 | setNeedsLayout() 162 | } 163 | 164 | open func prepareForReuse() { 165 | photo = nil 166 | if captionView != nil { 167 | captionView.removeFromSuperview() 168 | captionView = nil 169 | } 170 | } 171 | 172 | open func displayImage(_ image: UIImage) { 173 | // image 174 | imageView.image = image 175 | imageView.contentMode = photo.contentMode 176 | 177 | var imageViewFrame: CGRect = .zero 178 | imageViewFrame.origin = .zero 179 | // long photo 180 | if SKPhotoBrowserOptions.longPhotoWidthMatchScreen && image.size.height >= image.size.width { 181 | let imageHeight = SKMesurement.screenWidth / image.size.width * image.size.height 182 | imageViewFrame.size = CGSize(width: SKMesurement.screenWidth, height: imageHeight) 183 | } else { 184 | imageViewFrame.size = image.size 185 | } 186 | imageView.frame = imageViewFrame 187 | 188 | contentSize = imageViewFrame.size 189 | setMaxMinZoomScalesForCurrentBounds() 190 | } 191 | 192 | // MARK: - image 193 | open func displayImage(complete flag: Bool) { 194 | // reset scale 195 | maximumZoomScale = 1 196 | minimumZoomScale = 1 197 | zoomScale = 1 198 | 199 | if !flag { 200 | if photo.underlyingImage == nil { 201 | indicatorView.startAnimating() 202 | } 203 | photo.loadUnderlyingImageAndNotify() 204 | } else { 205 | indicatorView.stopAnimating() 206 | } 207 | 208 | if let image = photo.underlyingImage, photo != nil { 209 | displayImage(image) 210 | } else { 211 | // change contentSize will reset contentOffset, so only set the contentsize zero when the image is nil 212 | contentSize = CGSize.zero 213 | } 214 | setNeedsLayout() 215 | } 216 | 217 | open func displayImageFailure() { 218 | indicatorView.stopAnimating() 219 | } 220 | 221 | // MARK: - handle tap 222 | open func handleDoubleTap(_ touchPoint: CGPoint) { 223 | if let browser = browser { 224 | NSObject.cancelPreviousPerformRequests(withTarget: browser) 225 | } 226 | 227 | if zoomScale > minimumZoomScale { 228 | // zoom out 229 | setZoomScale(minimumZoomScale, animated: true) 230 | } else { 231 | // zoom in 232 | // I think that the result should be the same after double touch or pinch 233 | /* var newZoom: CGFloat = zoomScale * 3.13 234 | if newZoom >= maximumZoomScale { 235 | newZoom = maximumZoomScale 236 | } 237 | */ 238 | let zoomRect = zoomRectForScrollViewWith(maximumZoomScale, touchPoint: touchPoint) 239 | zoom(to: zoomRect, animated: true) 240 | } 241 | 242 | // delay control 243 | browser?.hideControlsAfterDelay() 244 | } 245 | } 246 | 247 | // MARK: - UIScrollViewDelegate 248 | 249 | extension SKZoomingScrollView: UIScrollViewDelegate { 250 | public func viewForZooming(in scrollView: UIScrollView) -> UIView? { 251 | return imageView 252 | } 253 | 254 | public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { 255 | browser?.cancelControlHiding() 256 | } 257 | 258 | public func scrollViewDidZoom(_ scrollView: UIScrollView) { 259 | setNeedsLayout() 260 | layoutIfNeeded() 261 | } 262 | } 263 | 264 | // MARK: - SKDetectingImageViewDelegate 265 | 266 | extension SKZoomingScrollView: SKDetectingViewDelegate { 267 | func handleSingleTap(_ view: UIView, touch: UITouch) { 268 | guard let browser = browser else { 269 | return 270 | } 271 | guard SKPhotoBrowserOptions.enableZoomBlackArea == true else { 272 | return 273 | } 274 | 275 | if browser.areControlsHidden() == false && SKPhotoBrowserOptions.enableSingleTapDismiss == true { 276 | browser.determineAndClose() 277 | } else { 278 | browser.toggleControls() 279 | } 280 | } 281 | 282 | func handleDoubleTap(_ view: UIView, touch: UITouch) { 283 | if SKPhotoBrowserOptions.enableZoomBlackArea == true { 284 | let needPoint = getViewFramePercent(view, touch: touch) 285 | handleDoubleTap(needPoint) 286 | } 287 | } 288 | } 289 | 290 | // MARK: - SKDetectingImageViewDelegate 291 | 292 | extension SKZoomingScrollView: SKDetectingImageViewDelegate { 293 | func handleImageViewSingleTap(_ touchPoint: CGPoint) { 294 | guard let browser = browser else { 295 | return 296 | } 297 | if SKPhotoBrowserOptions.enableSingleTapDismiss { 298 | browser.determineAndClose() 299 | } else { 300 | browser.toggleControls() 301 | } 302 | } 303 | 304 | func handleImageViewDoubleTap(_ touchPoint: CGPoint) { 305 | handleDoubleTap(touchPoint) 306 | } 307 | } 308 | 309 | private extension SKZoomingScrollView { 310 | func getViewFramePercent(_ view: UIView, touch: UITouch) -> CGPoint { 311 | let oneWidthViewPercent = view.bounds.width / 100 312 | let viewTouchPoint = touch.location(in: view) 313 | let viewWidthTouch = viewTouchPoint.x 314 | let viewPercentTouch = viewWidthTouch / oneWidthViewPercent 315 | let photoWidth = imageView.bounds.width 316 | let onePhotoPercent = photoWidth / 100 317 | let needPoint = viewPercentTouch * onePhotoPercent 318 | 319 | var Y: CGFloat! 320 | 321 | if viewTouchPoint.y < view.bounds.height / 2 { 322 | Y = 0 323 | } else { 324 | Y = imageView.bounds.height 325 | } 326 | let allPoint = CGPoint(x: needPoint, y: Y) 327 | return allPoint 328 | } 329 | 330 | func zoomRectForScrollViewWith(_ scale: CGFloat, touchPoint: CGPoint) -> CGRect { 331 | let w = frame.size.width / scale 332 | let h = frame.size.height / scale 333 | let x = touchPoint.x - (h / max(SKMesurement.screenScale, 2.0)) 334 | let y = touchPoint.y - (w / max(SKMesurement.screenScale, 2.0)) 335 | 336 | return CGRect(x: x, y: y, width: w, height: h) 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

SKPhotoBrowser

2 | 3 |

4 | Simple PhotoBrowser/Viewer inspired by facebook, twitter photo browsers written by swift 5 |

6 | 7 |

8 | 9 | Swift5 10 | 11 | 12 | 13 | 14 | 15 | Build Status 16 | 17 | 18 | Platform 19 | 20 | Contributors 21 |

22 | 23 | 24 | ## features 25 | - Display one or more images by providing either `UIImage` objects, or string of URL array. 26 | - Photos can be zoomed and panned, and optional captions can be displayed 27 | - Minimalistic Facebook-like interface, swipe up/down to dismiss 28 | - Ability to custom control. (hide/ show toolbar for controls, / swipe control) 29 | - Handling and caching photos from web 30 | - Landscape handling 31 | - Delete photo support(by offbye). By set displayDelete=true show a delete icon in statusbar, deleted indexes can be obtain from delegate func didDeleted 32 | 33 | | Table/CollectionView sample | Button tap sample | gif sample | 34 | | ------------- | --------------- | --------------| 35 | | ![sample](Screenshots/example01.gif) | ![sample](Screenshots/example02.gif) | ![sample](Screenshots/example03.gif) 36 | 37 | ## Requirements 38 | - iOS 9.0+ 39 | - Swift 2.0+ 40 | - ARC 41 | 42 | ### Version vs Swift version. 43 | 44 | Below is a table that shows which version of SKPhotoBrowser you should use for your Swift version. 45 | 46 | | Swift version | SKPhotoBrowser version | 47 | | ------------- | ---------------| 48 | | 5.0 | >= 6.1.0 | 49 | | 4.2 | >= 6.0.0 | 50 | | 4.1 | >= 5.0.0 | 51 | | 3.2 | >= 4.0.0 | 52 | | 2.3 | 2.0.4 - 3.1.4 | 53 | | 2.2 | <= 2.0.3 | 54 | 55 | ## Installation 56 | 57 | #### CocoaPods 58 | available on CocoaPods. Just add the following to your project Podfile: 59 | ``` 60 | pod 'SKPhotoBrowser' 61 | use_frameworks! 62 | ``` 63 | 64 | #### Carthage 65 | To integrate into your Xcode project using Carthage, specify it in your Cartfile: 66 | ``` 67 | github "suzuki-0000/SKPhotoBrowser" 68 | ``` 69 | 70 | #### Info.plist 71 | If you want to use share image feature, it includes save image into galery, so you should specify a permission into your Info.plist (if you haven't done it yet). 72 | ``` 73 | NSPhotoLibraryAddUsageDescription 74 | Used to save images into your galery 75 | ``` 76 | 77 | #### Swift Package Manager 78 | Available in Swift Package Manager. Use the repository URL in Xcode 79 | 80 | ## Usage 81 | See the code snippet below for an example of how to implement, or see the example project. 82 | 83 | from UIImages: 84 | ```swift 85 | // 1. create SKPhoto Array from UIImage 86 | var images = [SKPhoto]() 87 | let photo = SKPhoto.photoWithImage(UIImage())// add some UIImage 88 | images.append(photo) 89 | 90 | // 2. create PhotoBrowser Instance, and present from your viewController. 91 | let browser = SKPhotoBrowser(photos: images) 92 | browser.initializePageIndex(0) 93 | present(browser, animated: true, completion: {}) 94 | ``` 95 | 96 | from URLs: 97 | ```swift 98 | // 1. create URL Array 99 | var images = [SKPhoto]() 100 | let photo = SKPhoto.photoWithImageURL("https://placehold.jp/150x150.png") 101 | photo.shouldCachePhotoURLImage = false // you can use image cache by true(NSCache) 102 | images.append(photo) 103 | 104 | // 2. create PhotoBrowser Instance, and present. 105 | let browser = SKPhotoBrowser(photos: images) 106 | browser.initializePageIndex(0) 107 | present(browser, animated: true, completion: {}) 108 | ``` 109 | 110 | from local files: 111 | ```swift 112 | // 1. create images from local files 113 | var images = [SKLocalPhoto]() 114 | let photo = SKLocalPhoto.photoWithImageURL("..some_local_path/150x150.png") 115 | images.append(photo) 116 | 117 | // 2. create PhotoBrowser Instance, and present. 118 | let browser = SKPhotoBrowser(photos: images) 119 | browser.initializePageIndex(0) 120 | present(browser, animated: true, completion: {}) 121 | ``` 122 | 123 | If you want to use zooming effect from an existing view, use another initializer: 124 | ```swift 125 | // e.g.: some tableView or collectionView. 126 | func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { 127 | let cell = collectionView.cellForItemAtIndexPath(indexPath) 128 | let originImage = cell.exampleImageView.image // some image for baseImage 129 | 130 | let browser = SKPhotoBrowser(originImage: originImage ?? UIImage(), photos: images, animatedFromView: cell) 131 | browser.initializePageIndex(indexPath.row) 132 | present(browser, animated: true, completion: {}) 133 | } 134 | ``` 135 | 136 | ### Custom 137 | 138 | #### Toolbar 139 | You can customize Toolbar via SKPhotoBrowserOptions. 140 | 141 | ```swift 142 | SKPhotoBrowserOptions.displayToolbar = false // all tool bar will be hidden 143 | SKPhotoBrowserOptions.displayCounterLabel = false // counter label will be hidden 144 | SKPhotoBrowserOptions.displayBackAndForwardButton = false // back / forward button will be hidden 145 | SKPhotoBrowserOptions.displayAction = false // action button will be hidden 146 | SKPhotoBrowserOptions.displayHorizontalScrollIndicator = false // horizontal scroll bar will be hidden 147 | SKPhotoBrowserOptions.displayVerticalScrollIndicator = false // vertical scroll bar will be hidden 148 | let browser = SKPhotoBrowser(originImage: originImage, photos: images, animatedFromView: cell) 149 | ``` 150 | 151 | #### Colors 152 | You can customize text, icon and background colors via SKPhotoBrowserOptions or SKToolbarOptions 153 | ```swift 154 | SKPhotoBrowserOptions.backgroundColor = UIColor.whiteColor() // browser view will be white 155 | SKPhotoBrowserOptions.textAndIconColor = UIColor.blackColor() // text and icons will be black 156 | SKToolbarOptions.textShadowColor = UIColor.clearColor() // shadow of toolbar text will be removed 157 | SKToolbarOptions.font = UIFont(name: "Futura", size: 16.0) // font of toolbar will be 'Futura' 158 | ``` 159 | 160 | #### Images 161 | You can customize the padding of displayed images via SKPhotoBrowserOptions 162 | ```swift 163 | SKPhotoBrowserOptions.imagePaddingX = 50 // image padding left and right will be 25 164 | SKPhotoBrowserOptions.imagePaddingY = 50 // image padding top and bottom will be 25 165 | ``` 166 | 167 | #### Statusbar 168 | You can customize the visibility of the Statusbar in browser view via SKPhotoBrowserOptions 169 | ```swift 170 | SKPhotoBrowserOptions.displayStatusbar = false // status bar will be hidden 171 | ``` 172 | 173 | #### Close And Delete Buttons 174 | That how you can customize close and delete buttons 175 | ```swift 176 | SKPhotoBrowserOptions.displayDeleteButton = true // delete button will be shown 177 | SKPhotoBrowserOptions.swapCloseAndDeleteButtons = true // now close button located on right side of screen and delete button is on left side 178 | SKPhotoBrowserOptions.closeAndDeleteButtonPadding = 20 // set offset from top and from nearest screen edge of close button and delete button 179 | ``` 180 | 181 | #### Screenshot Protection 182 | You can protect your image from taking screenshot via SKPhotoBrowserOptions 183 | Only working on the device, not on simulator 184 | ```swift 185 | SKPhotoBrowserOptions.protectScreenshot = true // image will be hidden after taking screenshot 186 | ``` 187 | 188 | #### Custom Cache From Web URL 189 | You can use SKCacheable protocol if others are adaptable. (SKImageCacheable or SKRequestResponseCacheable) 190 | 191 | ```swift 192 | e.g. SDWebImage 193 | 194 | // 1. create custom cache, implement in accordance with the protocol 195 | class CustomImageCache: SKImageCacheable { var cache: SDImageCache } 196 | 197 | // 2. replace SKCache instance with custom cache 198 | SKCache.sharedCache.imageCache = CustomImageCache() 199 | ``` 200 | 201 | #### CustomButton Image 202 | Close, Delete buttons are able to change image and frame. 203 | ```swift 204 | browser.updateCloseButton(UIImage()) 205 | browser.updateUpdateButton(UIImage()) 206 | ``` 207 | 208 | #### Delete Photo 209 | You can delete your photo for your own handling. detect button tap from `removePhoto` delegate function. 210 | 211 | 212 | #### Photo Captions 213 | Photo captions can be displayed simply bottom of PhotoBrowser. by setting the `caption` property on specific photos: 214 | ``` swift 215 | let photo = SKPhoto.photoWithImage(UIImage()) 216 | photo.caption = "Lorem Ipsum is simply dummy text of the printing and typesetting industry." 217 | ``` 218 | 219 | #### SwipeGesture 220 | vertical swipe can enable/disable: 221 | ``` swift 222 | SKPhotoBrowserOptions.disableVerticalSwipe = true 223 | ``` 224 | 225 | #### Delegate 226 | There's some trigger point you can handle using delegate. those are optional. 227 | See [SKPhotoBrowserDelegate](https://github.com/suzuki-0000/SKPhotoBrowser/blob/master/SKPhotoBrowser/SKPhotoBrowserDelegate.swift) for more details. 228 | - didShowPhotoAtIndex(_ index:Int) 229 | - willDismissAtPageIndex(_ index:Int) 230 | - willShowActionSheet(_ photoIndex: Int) 231 | - didDismissAtPageIndex(_ index:Int) 232 | - didDismissActionSheetWithButtonIndex(_ buttonIndex: Int, photoIndex: Int) 233 | - didScrollToIndex(_ index: Int) 234 | - removePhoto(_ browser: SKPhotoBrowser, index: Int, reload: (() -> Void)) 235 | - viewForPhoto(_ browser: SKPhotoBrowser, index: Int) -> UIView? 236 | - controlsVisibilityToggled(_ browser: SKPhotoBrowser, hidden: Bool) 237 | 238 | ```swift 239 | let browser = SKPhotoBrowser(originImage: originImage, photos: images, animatedFromView: cell) 240 | browser.delegate = self 241 | 242 | // MARK: - SKPhotoBrowserDelegate 243 | func didShowPhotoAtIndex(_ index: Int) { 244 | // when photo will be shown 245 | } 246 | 247 | func willDismissAtPageIndex(_ index: Int) { 248 | // when PhotoBrowser will be dismissed 249 | } 250 | 251 | func didDismissAtPageIndex(_ index: Int) { 252 | // when PhotoBrowser did dismissed 253 | } 254 | 255 | ``` 256 | 257 | #### Options 258 | You can access via `SKPhotoBrowserOptions`, which can use for browser control. 259 | See [SKPhotoBrowserOptions](https://github.com/suzuki-0000/SKPhotoBrowser/blob/master/SKPhotoBrowser/SKPhotoBrowserOptions.swift) for more details. 260 | - single tap handling, dismiss/noaction 261 | - blackArea handling which is appearing outside of photo 262 | - bounce animation when appearing/dismissing 263 | - text color, font, or more 264 | ``` swift 265 | SKPhotoBrowserOptions.enableZoomBlackArea = true // default true 266 | SKPhotoBrowserOptions.enableSingleTapDismiss = true // default false 267 | ``` 268 | 269 | ## Photos from 270 | - [Unsplash](https://unsplash.com) 271 | 272 | ## License 273 | available under the MIT license. See the LICENSE file for more info. 274 | 275 | ## Contributors 276 | 277 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 |

Oreo Chen

💻
286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | | [
Alexander Khitev](https://github.com/alexanderkhitev)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=alexanderkhitev "Code") | [
K Rummler](https://github.com/krummler)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=krummler "Code") | [
Mads Bjerre](http://wisekopf.com)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=madsb "Code") | [
Meng Ye](https://jk2K.com)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=jk2K "Code") | [
_ant_one](https://github.com/barrault01)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=barrault01 "Code") | [
Tim Roesner](http://timroesner.com)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=timroesner "Code") | [
胥冥](http://www.zxming.com)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=zxming "Code") | 295 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 296 | | [
Kevin Wolkober](http://kevinwo.github.io)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=kevinwo "Code") | [
PJ Gray](http://www.saygoodnight.com/)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=pj4533 "Code") | [
ghysrc](https://github.com/ghysrc)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=ghysrc "Code") | [
Josef Doležal](http://josefdolezal.github.com)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=josefdolezal "Code") | [
Mark Goody](https://marramgrass.micro.blog/)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=marramgrass "Code") | [
Philippe Riegert](https://github.com/PhilippeRiegert)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=PhilippeRiegert "Code") | [
Bryan Irace](http://irace.me)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=irace "Code") | 297 | | [
dirtmelon](https://github.com/dirtmelon)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=dirtmelon "Code") | [
Heberti Almeida](https://dribbble.com/hebertialmeida)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=hebertialmeida "Code") | [
Felix Weiss](http://othellogame.net)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=appsunited "Code") | [
.Some](https://github.com/BigDanceMouse)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=BigDanceMouse "Code") | [
Onur Var](https://tr.linkedin.com/in/onur-var)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=OnurVar "Code") | [
Andrew Barba](https://abarba.me)
[💻](https://github.com/suzuki-0000/SKPhotoBrowser/commits?author=AndrewBarba "Code") | 298 | 299 | 300 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 301 | -------------------------------------------------------------------------------- /SKPhotoBrowserExample/SKPhotoBrowserExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 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 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 96 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/runtime@^7.14.6", "@babel/runtime@^7.7.6": 6 | version "7.14.6" 7 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" 8 | integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== 9 | dependencies: 10 | regenerator-runtime "^0.13.4" 11 | 12 | all-contributors-cli@^6.20.0: 13 | version "6.20.0" 14 | resolved "https://registry.yarnpkg.com/all-contributors-cli/-/all-contributors-cli-6.20.0.tgz#9bc98dda38cb29cfe8afc8a78c004e14af25d2f6" 15 | integrity sha512-trEQlL1s1u8FSWSwY2w9uL4GCG7Fo9HIW5rm5LtlE0SQHSolfXQBzJib07Qes5j52/t72wjuE6sEKkuRrwiuuQ== 16 | dependencies: 17 | "@babel/runtime" "^7.7.6" 18 | async "^3.0.1" 19 | chalk "^4.0.0" 20 | didyoumean "^1.2.1" 21 | inquirer "^7.0.4" 22 | json-fixer "^1.5.1" 23 | lodash "^4.11.2" 24 | node-fetch "^2.6.0" 25 | pify "^5.0.0" 26 | yargs "^15.0.1" 27 | 28 | ansi-escapes@^4.2.1: 29 | version "4.3.2" 30 | resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" 31 | integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== 32 | dependencies: 33 | type-fest "^0.21.3" 34 | 35 | ansi-regex@^5.0.0: 36 | version "5.0.1" 37 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 38 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 39 | 40 | ansi-styles@^4.0.0, ansi-styles@^4.1.0: 41 | version "4.3.0" 42 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 43 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 44 | dependencies: 45 | color-convert "^2.0.1" 46 | 47 | async@^3.0.1: 48 | version "3.2.4" 49 | resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" 50 | integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== 51 | 52 | camelcase@^5.0.0: 53 | version "5.3.1" 54 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" 55 | integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== 56 | 57 | chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: 58 | version "4.1.1" 59 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" 60 | integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== 61 | dependencies: 62 | ansi-styles "^4.1.0" 63 | supports-color "^7.1.0" 64 | 65 | chardet@^0.7.0: 66 | version "0.7.0" 67 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" 68 | integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== 69 | 70 | cli-cursor@^3.1.0: 71 | version "3.1.0" 72 | resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" 73 | integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== 74 | dependencies: 75 | restore-cursor "^3.1.0" 76 | 77 | cli-width@^3.0.0: 78 | version "3.0.0" 79 | resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" 80 | integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== 81 | 82 | cliui@^6.0.0: 83 | version "6.0.0" 84 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" 85 | integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== 86 | dependencies: 87 | string-width "^4.2.0" 88 | strip-ansi "^6.0.0" 89 | wrap-ansi "^6.2.0" 90 | 91 | color-convert@^2.0.1: 92 | version "2.0.1" 93 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 94 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 95 | dependencies: 96 | color-name "~1.1.4" 97 | 98 | color-name@~1.1.4: 99 | version "1.1.4" 100 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 101 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 102 | 103 | decamelize@^1.2.0: 104 | version "1.2.0" 105 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 106 | integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= 107 | 108 | didyoumean@^1.2.1: 109 | version "1.2.2" 110 | resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" 111 | integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== 112 | 113 | emoji-regex@^8.0.0: 114 | version "8.0.0" 115 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 116 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 117 | 118 | escape-string-regexp@^1.0.5: 119 | version "1.0.5" 120 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 121 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 122 | 123 | external-editor@^3.0.3: 124 | version "3.1.0" 125 | resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" 126 | integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== 127 | dependencies: 128 | chardet "^0.7.0" 129 | iconv-lite "^0.4.24" 130 | tmp "^0.0.33" 131 | 132 | figures@^3.0.0: 133 | version "3.2.0" 134 | resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" 135 | integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== 136 | dependencies: 137 | escape-string-regexp "^1.0.5" 138 | 139 | find-up@^4.1.0: 140 | version "4.1.0" 141 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" 142 | integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== 143 | dependencies: 144 | locate-path "^5.0.0" 145 | path-exists "^4.0.0" 146 | 147 | get-caller-file@^2.0.1: 148 | version "2.0.5" 149 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 150 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 151 | 152 | has-flag@^4.0.0: 153 | version "4.0.0" 154 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 155 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 156 | 157 | iconv-lite@^0.4.24: 158 | version "0.4.24" 159 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 160 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 161 | dependencies: 162 | safer-buffer ">= 2.1.2 < 3" 163 | 164 | inquirer@^7.0.4: 165 | version "7.3.3" 166 | resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" 167 | integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== 168 | dependencies: 169 | ansi-escapes "^4.2.1" 170 | chalk "^4.1.0" 171 | cli-cursor "^3.1.0" 172 | cli-width "^3.0.0" 173 | external-editor "^3.0.3" 174 | figures "^3.0.0" 175 | lodash "^4.17.19" 176 | mute-stream "0.0.8" 177 | run-async "^2.4.0" 178 | rxjs "^6.6.0" 179 | string-width "^4.1.0" 180 | strip-ansi "^6.0.0" 181 | through "^2.3.6" 182 | 183 | is-fullwidth-code-point@^3.0.0: 184 | version "3.0.0" 185 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 186 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 187 | 188 | json-fixer@^1.5.1: 189 | version "1.6.12" 190 | resolved "https://registry.yarnpkg.com/json-fixer/-/json-fixer-1.6.12.tgz#352026c905c6366e214c9f10f77d6d7f93c48322" 191 | integrity sha512-BGO9HExf0ZUVYvuWsps71Re513Ss0il1Wp7wYWkir2NthzincvNJEUu82KagEfAkGdjOMsypj3t2JB7drBKWnA== 192 | dependencies: 193 | "@babel/runtime" "^7.14.6" 194 | chalk "^4.1.1" 195 | pegjs "^0.10.0" 196 | 197 | locate-path@^5.0.0: 198 | version "5.0.0" 199 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" 200 | integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== 201 | dependencies: 202 | p-locate "^4.1.0" 203 | 204 | lodash@^4.11.2, lodash@^4.17.19: 205 | version "4.17.21" 206 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 207 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 208 | 209 | mimic-fn@^2.1.0: 210 | version "2.1.0" 211 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" 212 | integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== 213 | 214 | mute-stream@0.0.8: 215 | version "0.0.8" 216 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" 217 | integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== 218 | 219 | node-fetch@^2.6.0: 220 | version "2.6.7" 221 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" 222 | integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== 223 | dependencies: 224 | whatwg-url "^5.0.0" 225 | 226 | onetime@^5.1.0: 227 | version "5.1.2" 228 | resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" 229 | integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== 230 | dependencies: 231 | mimic-fn "^2.1.0" 232 | 233 | os-tmpdir@~1.0.2: 234 | version "1.0.2" 235 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 236 | integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= 237 | 238 | p-limit@^2.2.0: 239 | version "2.3.0" 240 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" 241 | integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== 242 | dependencies: 243 | p-try "^2.0.0" 244 | 245 | p-locate@^4.1.0: 246 | version "4.1.0" 247 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" 248 | integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== 249 | dependencies: 250 | p-limit "^2.2.0" 251 | 252 | p-try@^2.0.0: 253 | version "2.2.0" 254 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" 255 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== 256 | 257 | path-exists@^4.0.0: 258 | version "4.0.0" 259 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 260 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 261 | 262 | pegjs@^0.10.0: 263 | version "0.10.0" 264 | resolved "https://registry.yarnpkg.com/pegjs/-/pegjs-0.10.0.tgz#cf8bafae6eddff4b5a7efb185269eaaf4610ddbd" 265 | integrity sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0= 266 | 267 | pify@^5.0.0: 268 | version "5.0.0" 269 | resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" 270 | integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== 271 | 272 | regenerator-runtime@^0.13.4: 273 | version "0.13.7" 274 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" 275 | integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== 276 | 277 | require-directory@^2.1.1: 278 | version "2.1.1" 279 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 280 | integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 281 | 282 | require-main-filename@^2.0.0: 283 | version "2.0.0" 284 | resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" 285 | integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== 286 | 287 | restore-cursor@^3.1.0: 288 | version "3.1.0" 289 | resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" 290 | integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== 291 | dependencies: 292 | onetime "^5.1.0" 293 | signal-exit "^3.0.2" 294 | 295 | run-async@^2.4.0: 296 | version "2.4.1" 297 | resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" 298 | integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== 299 | 300 | rxjs@^6.6.0: 301 | version "6.6.7" 302 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" 303 | integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== 304 | dependencies: 305 | tslib "^1.9.0" 306 | 307 | "safer-buffer@>= 2.1.2 < 3": 308 | version "2.1.2" 309 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 310 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 311 | 312 | set-blocking@^2.0.0: 313 | version "2.0.0" 314 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 315 | integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= 316 | 317 | signal-exit@^3.0.2: 318 | version "3.0.3" 319 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" 320 | integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== 321 | 322 | string-width@^4.1.0, string-width@^4.2.0: 323 | version "4.2.2" 324 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" 325 | integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== 326 | dependencies: 327 | emoji-regex "^8.0.0" 328 | is-fullwidth-code-point "^3.0.0" 329 | strip-ansi "^6.0.0" 330 | 331 | strip-ansi@^6.0.0: 332 | version "6.0.0" 333 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" 334 | integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== 335 | dependencies: 336 | ansi-regex "^5.0.0" 337 | 338 | supports-color@^7.1.0: 339 | version "7.2.0" 340 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 341 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 342 | dependencies: 343 | has-flag "^4.0.0" 344 | 345 | through@^2.3.6: 346 | version "2.3.8" 347 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 348 | integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= 349 | 350 | tmp@^0.0.33: 351 | version "0.0.33" 352 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" 353 | integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== 354 | dependencies: 355 | os-tmpdir "~1.0.2" 356 | 357 | tr46@~0.0.3: 358 | version "0.0.3" 359 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 360 | integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== 361 | 362 | tslib@^1.9.0: 363 | version "1.14.1" 364 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" 365 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 366 | 367 | type-fest@^0.21.3: 368 | version "0.21.3" 369 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" 370 | integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== 371 | 372 | webidl-conversions@^3.0.0: 373 | version "3.0.1" 374 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 375 | integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== 376 | 377 | whatwg-url@^5.0.0: 378 | version "5.0.0" 379 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 380 | integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== 381 | dependencies: 382 | tr46 "~0.0.3" 383 | webidl-conversions "^3.0.0" 384 | 385 | which-module@^2.0.0: 386 | version "2.0.0" 387 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 388 | integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= 389 | 390 | wrap-ansi@^6.2.0: 391 | version "6.2.0" 392 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" 393 | integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== 394 | dependencies: 395 | ansi-styles "^4.0.0" 396 | string-width "^4.1.0" 397 | strip-ansi "^6.0.0" 398 | 399 | y18n@^4.0.0: 400 | version "4.0.3" 401 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" 402 | integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== 403 | 404 | yargs-parser@^18.1.2: 405 | version "18.1.3" 406 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" 407 | integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== 408 | dependencies: 409 | camelcase "^5.0.0" 410 | decamelize "^1.2.0" 411 | 412 | yargs@^15.0.1: 413 | version "15.4.1" 414 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" 415 | integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== 416 | dependencies: 417 | cliui "^6.0.0" 418 | decamelize "^1.2.0" 419 | find-up "^4.1.0" 420 | get-caller-file "^2.0.1" 421 | require-directory "^2.1.1" 422 | require-main-filename "^2.0.0" 423 | set-blocking "^2.0.0" 424 | string-width "^4.2.0" 425 | which-module "^2.0.0" 426 | y18n "^4.0.0" 427 | yargs-parser "^18.1.2" 428 | --------------------------------------------------------------------------------