marinofaggiana 5 years ago
parent
commit
9c642eeda2

+ 0 - 1
Cartfile

@@ -22,4 +22,3 @@ github "ivanbruel/MarkdownKit"
 
 github "https://github.com/marinofaggiana/FastScroll" "master"
 github "https://github.com/marinofaggiana/AFNetworking" "master"
-github "https://github.com/marinofaggiana/ATGMediaBrowser" "master"

+ 24 - 3
Nextcloud.xcodeproj/project.pbxproj

@@ -440,7 +440,10 @@
 		F78F74342163757000C2ADAD /* NCTrash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F78F74332163757000C2ADAD /* NCTrash.storyboard */; };
 		F78F74362163781100C2ADAD /* NCTrash.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78F74352163781100C2ADAD /* NCTrash.swift */; };
 		F790110E21415BF600D7B136 /* NCViewerRichdocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = F790110D21415BF600D7B136 /* NCViewerRichdocument.swift */; };
-		F79018A524092EF4007C9B6D /* ATGMediaBrowser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F79018A424092EF4007C9B6D /* ATGMediaBrowser.framework */; };
+		F79018B6240962C7007C9B6D /* DismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79018B2240962C7007C9B6D /* DismissAnimationController.swift */; };
+		F79018B7240962C7007C9B6D /* MediaContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79018B3240962C7007C9B6D /* MediaContentView.swift */; };
+		F79018B8240962C7007C9B6D /* MediaBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79018B4240962C7007C9B6D /* MediaBrowserViewController.swift */; };
+		F79018B9240962C7007C9B6D /* ContentTransformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79018B5240962C7007C9B6D /* ContentTransformers.swift */; };
 		F79630EE215527D40015EEA5 /* NCViewerMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79630ED215527D40015EEA5 /* NCViewerMedia.swift */; };
 		F79728D422F96F2E003CACA7 /* NCShareLinkFolderMenuView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F79728D322F96F2D003CACA7 /* NCShareLinkFolderMenuView.xib */; };
 		F79728D622F9A0B1003CACA7 /* NCShareUserFolderMenuView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F79728D522F9A0B0003CACA7 /* NCShareUserFolderMenuView.xib */; };
@@ -1136,6 +1139,10 @@
 		F78F74352163781100C2ADAD /* NCTrash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrash.swift; sourceTree = "<group>"; };
 		F790110D21415BF600D7B136 /* NCViewerRichdocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCViewerRichdocument.swift; sourceTree = "<group>"; };
 		F79018A424092EF4007C9B6D /* ATGMediaBrowser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ATGMediaBrowser.framework; path = Carthage/Build/iOS/ATGMediaBrowser.framework; sourceTree = "<group>"; };
+		F79018B2240962C7007C9B6D /* DismissAnimationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissAnimationController.swift; sourceTree = "<group>"; };
+		F79018B3240962C7007C9B6D /* MediaContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaContentView.swift; sourceTree = "<group>"; };
+		F79018B4240962C7007C9B6D /* MediaBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaBrowserViewController.swift; sourceTree = "<group>"; };
+		F79018B5240962C7007C9B6D /* ContentTransformers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentTransformers.swift; sourceTree = "<group>"; };
 		F7956FC91B4886E60085DEA3 /* CCUploadFromOtherUpp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CCUploadFromOtherUpp.h; sourceTree = "<group>"; };
 		F7956FCA1B4886E60085DEA3 /* CCUploadFromOtherUpp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CCUploadFromOtherUpp.m; sourceTree = "<group>"; };
 		F7956FCB1B4886E60085DEA3 /* CCUploadFromOtherUpp.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = CCUploadFromOtherUpp.storyboard; sourceTree = "<group>"; };
@@ -1375,7 +1382,6 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				F79018A524092EF4007C9B6D /* ATGMediaBrowser.framework in Frameworks */,
 				F716FE7823795E5000FABE50 /* NCCommunication.framework in Frameworks */,
 				F74C4FBB2328C3C200A23E25 /* OpenSSL.framework in Frameworks */,
 				F7421EAF2294044B00C4B7C1 /* Accelerate.framework in Frameworks */,
@@ -2168,9 +2174,21 @@
 			path = Trash;
 			sourceTree = "<group>";
 		};
+		F79018B1240962C7007C9B6D /* NCMediaBrowser */ = {
+			isa = PBXGroup;
+			children = (
+				F79018B2240962C7007C9B6D /* DismissAnimationController.swift */,
+				F79018B3240962C7007C9B6D /* MediaContentView.swift */,
+				F79018B4240962C7007C9B6D /* MediaBrowserViewController.swift */,
+				F79018B5240962C7007C9B6D /* ContentTransformers.swift */,
+			);
+			path = NCMediaBrowser;
+			sourceTree = "<group>";
+		};
 		F79630EC215526B60015EEA5 /* Viewer */ = {
 			isa = PBXGroup;
 			children = (
+				F79018B1240962C7007C9B6D /* NCMediaBrowser */,
 				F72D404823D2082500A97FD0 /* NCViewerNextcloudText.swift */,
 				F710D1F42405770F00A6033D /* NCViewerPDF.swift */,
 				F790110D21415BF600D7B136 /* NCViewerRichdocument.swift */,
@@ -3170,7 +3188,6 @@
 				"$(SRCROOT)/Carthage/Build/iOS/SwiftEntryKit.framework",
 				"$(SRCROOT)/Carthage/Build/iOS/FloatingPanel.framework",
 				"$(SRCROOT)/Carthage/Build/iOS/MarkdownKit.framework",
-				"$(SRCROOT)/Carthage/Build/iOS/ATGMediaBrowser.framework",
 			);
 			outputPaths = (
 			);
@@ -3384,6 +3401,7 @@
 				F77B0E041D118A16002130FE /* UIImage+animatedGIF.m in Sources */,
 				37ECC83B23D0C7410082EFA2 /* NCMenuAction.swift in Sources */,
 				F73B4EFE1F470D9100BBEE4B /* LangHungarianModel.cpp in Sources */,
+				F79018B8240962C7007C9B6D /* MediaBrowserViewController.swift in Sources */,
 				F769454022E9F077000A798A /* NCSharePaging.swift in Sources */,
 				F760F79921F21F61006B1A73 /* CropViewController.swift in Sources */,
 				F70022EC1EC4C9100080073F /* OCXMLSharedParser.m in Sources */,
@@ -3397,6 +3415,7 @@
 				F73F537F1E929C8500F8678D /* CCMore.swift in Sources */,
 				F73B4EF71F470D9100BBEE4B /* LangBulgarianModel.cpp in Sources */,
 				F7F54D0C1E5B14C800E19C62 /* MWTapDetectingView.m in Sources */,
+				F79018B7240962C7007C9B6D /* MediaContentView.swift in Sources */,
 				F7FB1D3E215E191D00D669EA /* NCViewerDocumentWeb.swift in Sources */,
 				F78E7065219F096B006F23E4 /* NCAvatar.swift in Sources */,
 				F7DFB7F0219C5B8000680748 /* NCCreateFormUploadAssets.swift in Sources */,
@@ -3564,6 +3583,7 @@
 				F70022D41EC4C9100080073F /* NSDate+ISO8601.m in Sources */,
 				F762CB151EACB66200B38484 /* XLFormRowNavigationAccessoryView.m in Sources */,
 				F762CB0A1EACB66200B38484 /* XLFormDescriptor.m in Sources */,
+				F79018B6240962C7007C9B6D /* DismissAnimationController.swift in Sources */,
 				F7020FCE2233D7F700B7297D /* NCCreateFormUploadVoiceNote.swift in Sources */,
 				F7F4B1D823C74B3E00D82A6E /* NCRichWorkspace.swift in Sources */,
 				F726EEEC1FED1C820030B9C8 /* NCEndToEndInitialize.swift in Sources */,
@@ -3580,6 +3600,7 @@
 				F707C26521A2DC5200F6181E /* NCStoreReview.swift in Sources */,
 				F73B4EFF1F470D9100BBEE4B /* LangRussianModel.cpp in Sources */,
 				F7BAADCB1ED5A87C00B7EAD4 /* NCManageDatabase.swift in Sources */,
+				F79018B9240962C7007C9B6D /* ContentTransformers.swift in Sources */,
 				F7A321551E9E2A070069AD1B /* CCFavorites.m in Sources */,
 				F704FA5C232A343F00BBA952 /* IMImagemeterViewer.swift in Sources */,
 				F73B4F031F470D9100BBEE4B /* LangVietnameseModel.cpp in Sources */,

+ 1 - 3
iOSClient/Main/NCDetailViewController.swift

@@ -24,7 +24,7 @@
 import Foundation
 import WebKit
 import NCCommunication
-import ATGMediaBrowser
+//import ATGMediaBrowser
 
 class NCDetailViewController: UIViewController, MediaBrowserViewControllerDelegate, MediaBrowserViewControllerDataSource {
     
@@ -124,8 +124,6 @@ class NCDetailViewController: UIViewController, MediaBrowserViewControllerDelega
             if mediaBrowser != nil {
                 mediaBrowser!.shouldShowPageControl = false
                 mediaBrowser!.enableInteractiveDismissal = false
-                mediaBrowser!.hideCloseButton = true
-                mediaBrowser!.hideVisualEffectView = true
                 mediaBrowser!.view.frame = CGRect(x: 0, y: 0, width: backgroundView.frame.width, height: backgroundView.frame.height)
 
                 addChild(mediaBrowser!)

+ 234 - 0
iOSClient/Viewer/NCMediaBrowser/ContentTransformers.swift

@@ -0,0 +1,234 @@
+//
+//  ContentTransformers.swift
+//  ATGMediaBrowser
+//
+//  Created by Suraj Thomas K on 7/17/18.
+//  Copyright © 2018 Al Tayer Group LLC.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+//  and associated documentation files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use, copy, modify, merge, publish,
+//  distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in all copies or
+//  substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+//  BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+//  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+/**
+ Content transformer used for transition between media item views.
+
+ - parameter contentView: The content view on which transform corresponding to the position has to be applied.
+ - parameter position: Current position for the passed content view.
+
+ - note:
+    The trasnform to be applied on the contentView has to be dependent on the position passed.
+    The position value can be -ve, 0.0 or positive.
+
+    Try to visualize content views at -1.0[previous]=>0.0[current]=>1.0[next].
+
+    1. When position is -1.0, the content view should be at the place meant for previous view.
+
+    2. When the position is 0.0, the transform applied on the content view should make it visible full screen at origin.
+
+    3. When position is 1.0, the content view should be at the place meant for next view.
+
+    Be mindful of the drawing order, when designing new transitions.
+ */
+public typealias ContentTransformer = (_ contentView: UIView, _ position: CGFloat) -> Void
+
+// MARK: - Default Transitions
+
+/// An enumeration to hold default content transformers
+public enum DefaultContentTransformers {
+
+    /**
+     Horizontal move-in-out content transformer.
+
+     - Requires:
+         * GestureDirection: Horizontal
+    */
+    public static let horizontalMoveInOut: ContentTransformer = { contentView, position in
+
+        let widthIncludingGap = contentView.bounds.size.width + MediaContentView.interItemSpacing
+        contentView.transform = CGAffineTransform(translationX: widthIncludingGap * position, y: 0.0)
+    }
+
+    /**
+     Vertical move-in-out content transformer.
+
+     - Requires:
+        * GestureDirection: Vertical
+     */
+    public static let verticalMoveInOut: ContentTransformer = { contentView, position in
+
+        let heightIncludingGap = contentView.bounds.size.height + MediaContentView.interItemSpacing
+        contentView.transform = CGAffineTransform(translationX: 0.0, y: heightIncludingGap * position)
+    }
+
+    /**
+     Horizontal slide-out content transformer.
+
+     - Requires:
+        * GestureDirection: Horizontal
+        * DrawOrder: PreviousToNext
+     */
+    public static let horizontalSlideOut: ContentTransformer = { contentView, position in
+
+        var scale: CGFloat = 1.0
+        if position < -0.5 {
+            scale = 0.9
+        } else if -0.5...0.0 ~= Double(position) {
+            scale = 1.0 + (position * 0.2)
+        }
+        var transform = CGAffineTransform(scaleX: scale, y: scale)
+
+        let widthIncludingGap = contentView.bounds.size.width + MediaContentView.interItemSpacing
+        let x = position >= 0.0 ? widthIncludingGap * position : 0.0
+        transform = transform.translatedBy(x: x, y: 0.0)
+
+        contentView.transform = transform
+
+        let margin: CGFloat = 0.0000001
+        contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position))
+    }
+
+    /**
+     Vertical slide-out content transformer.
+
+     - Requires:
+         * GestureDirection: Vertical
+         * DrawOrder: PreviousToNext
+     */
+    public static let verticalSlideOut: ContentTransformer = { contentView, position in
+
+        var scale: CGFloat = 1.0
+        if position < -0.5 {
+            scale = 0.9
+        } else if -0.5...0.0 ~= Double(position) {
+            scale = 1.0 + (position * 0.2)
+        }
+        var transform = CGAffineTransform(scaleX: scale, y: scale)
+
+        let heightIncludingGap = contentView.bounds.size.height + MediaContentView.interItemSpacing
+        let y = position >= 0.0 ? heightIncludingGap * position : 0.0
+        transform = transform.translatedBy(x: 0.0, y: y)
+
+        contentView.transform = transform
+
+        let margin: CGFloat = 0.0000001
+        contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position))
+    }
+
+    /**
+     Horizontal slide-in content transformer.
+
+     - Requires:
+         * GestureDirection: Horizontal
+         * DrawOrder: NextToPrevious
+     */
+    public static let horizontalSlideIn: ContentTransformer = { contentView, position in
+
+        var scale: CGFloat = 1.0
+        if position > 0.5 {
+            scale = 0.9
+        } else if 0.0...0.5 ~= Double(position) {
+            scale = 1.0 - (position * 0.2)
+        }
+        var transform = CGAffineTransform(scaleX: scale, y: scale)
+
+        let widthIncludingGap = contentView.bounds.size.width + MediaContentView.interItemSpacing
+        let x = position > 0.0 ? 0.0 : widthIncludingGap * position
+        transform = transform.translatedBy(x: x, y: 0.0)
+
+        contentView.transform = transform
+
+        let margin: CGFloat = 0.0000001
+        contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position))
+    }
+
+    /**
+     Vertical slide-in content transformer.
+
+     - Requires:
+         * GestureDirection: Vertical
+         * DrawOrder: NextToPrevious
+     */
+    public static let verticalSlideIn: ContentTransformer = { contentView, position in
+
+        var scale: CGFloat = 1.0
+        if position > 0.5 {
+            scale = 0.9
+        } else if 0.0...0.5 ~= Double(position) {
+            scale = 1.0 - (position * 0.2)
+        }
+        var transform = CGAffineTransform(scaleX: scale, y: scale)
+
+        let heightIncludingGap = contentView.bounds.size.height + MediaContentView.interItemSpacing
+        let y = position > 0.0 ? 0.0 : heightIncludingGap * position
+        transform = transform.translatedBy(x: 0.0, y: y)
+
+        contentView.transform = transform
+
+        let margin: CGFloat = 0.0000001
+        contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position))
+    }
+
+    /**
+     Horizontal zoom-in-out content transformer.
+
+     - Requires:
+     * GestureDirection: Horizontal
+     */
+    public static let horizontalZoomInOut: ContentTransformer = { contentView, position in
+
+        let minScale: CGFloat = 0.5
+        // Scale factor is used to reduce the scale animation speed.
+        let scaleFactor: CGFloat = 0.5
+        var scale: CGFloat = CGFloat.maximum(minScale, 1.0 - abs(position * scaleFactor))
+
+        // Actual gap will be scaleFactor * 0.5 times of contentView.bounds.size.width.
+        let actualGap = contentView.bounds.size.width * scaleFactor * 0.5
+        let gapCorrector = MediaContentView.interItemSpacing - actualGap
+
+        let widthIncludingGap = contentView.bounds.size.width + gapCorrector
+        let translation = (widthIncludingGap * position)/scale
+
+        var transform = CGAffineTransform(scaleX: scale, y: scale)
+        transform = transform.translatedBy(x: translation, y: 0.0)
+
+        contentView.transform = transform
+    }
+
+    /**
+     Vertical zoom-in-out content transformer.
+
+     - Requires:
+     * GestureDirection: Vertical
+     */
+    public static let verticalZoomInOut: ContentTransformer = { contentView, position in
+
+        let minScale: CGFloat = 0.5
+        // Scale factor is used to reduce the scale animation speed.
+        let scaleFactor: CGFloat = 0.5
+        let scale: CGFloat = CGFloat.maximum(minScale, 1.0 - abs(position * scaleFactor))
+
+        // Actual gap will be scaleFactor * 0.5 times of contentView.bounds.size.height.
+        let actualGap = contentView.bounds.size.height * scaleFactor * 0.5
+        let gapCorrector = MediaContentView.interItemSpacing - actualGap
+
+        let heightIncludingGap = contentView.bounds.size.height + gapCorrector
+        let translation = (heightIncludingGap * position)/scale
+
+        var transform = CGAffineTransform(scaleX: scale, y: scale)
+        transform = transform.translatedBy(x: 0.0, y: translation)
+
+        contentView.transform = transform
+    }
+}

+ 261 - 0
iOSClient/Viewer/NCMediaBrowser/DismissAnimationController.swift

@@ -0,0 +1,261 @@
+//
+//  DismissAnimationController.swift
+//  ATGMediaBrowser
+//
+//  Created by Suraj Thomas K on 7/19/18.
+//  Copyright © 2018 Al Tayer Group LLC.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+//  and associated documentation files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use, copy, modify, merge, publish,
+//  distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in all copies or
+//  substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+//  BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+//  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal class DismissAnimationController: NSObject {
+
+    private enum Constants {
+
+        static let minimumVelocity: CGFloat = 15.0
+        static let minimumTranslation: CGFloat = 0.25
+        static let transitionDuration = 0.3
+        static let updateFrameRate: CGFloat = 60.0
+        static let transitionSpeedFactor: CGFloat = 0.15
+        static let minimumZoomDuringInteraction: CGFloat = 0.9
+    }
+
+    internal var image: UIImage?
+    internal let gestureDirection: MediaBrowserViewController.GestureDirection
+    internal weak var viewController: MediaBrowserViewController?
+    internal var interactionInProgress = false
+
+    private lazy var imageView = UIImageView()
+
+    private var timer: Timer?
+    private var distanceToMove: CGPoint = .zero
+    private var relativePosition: CGPoint = .zero
+    private var progressValue: CGFloat {
+        return (gestureDirection == .horizontal) ? relativePosition.y : relativePosition.x
+    }
+    private var shouldZoomOutOnInteraction = false
+
+    init(
+        image: UIImage? = nil,
+        gestureDirection: MediaBrowserViewController.GestureDirection,
+        viewController: MediaBrowserViewController
+        ) {
+
+        self.image = image
+        self.gestureDirection = gestureDirection
+        self.viewController = viewController
+    }
+
+    internal func handleInteractiveTransition(_ recognizer: UIPanGestureRecognizer) {
+
+        let translation = recognizer.translation(in: recognizer.view)
+
+        let progress = CGPoint(
+            x: translation.x / UIScreen.main.bounds.size.width,
+            y: translation.y / UIScreen.main.bounds.size.height
+        )
+
+        switch recognizer.state {
+        case .began:
+            beginTransition()
+            fallthrough
+        case .changed:
+            relativePosition = progress
+            updateTransition()
+        case .ended, .cancelled, .failed:
+            var toMove: CGFloat = 0.0
+
+            if abs(progressValue) > Constants.minimumTranslation {
+                if let viewController = viewController,
+                    let targetFrame = viewController.dataSource?.targetFrameForDismissal(viewController) {
+
+                    animateToTargetFrame(targetFrame)
+                    return
+
+                } else {
+                    toMove = (progressValue / abs(progressValue))
+                }
+            } else {
+                toMove = -progressValue
+            }
+
+            if gestureDirection == .horizontal {
+                distanceToMove.x = -relativePosition.x
+                distanceToMove.y = toMove
+            } else {
+                distanceToMove.x = toMove
+                distanceToMove.y = -relativePosition.y
+            }
+
+            if timer == nil {
+                timer = Timer.scheduledTimer(
+                    timeInterval: 1.0/Double(Constants.updateFrameRate),
+                    target: self,
+                    selector: #selector(update(_:)),
+                    userInfo: nil,
+                    repeats: true
+                )
+            }
+        default:
+            break
+        }
+    }
+
+    internal func animateToTargetFrame(_ target: CGRect) {
+
+        let frame = imageViewFrame(for: imageView.bounds.size, in: target, mode: .scaleAspectFill)
+        UIView.animate(withDuration: Constants.transitionDuration, animations: {
+            
+            self.imageView.frame = frame
+            
+        }) { finished in
+
+            if finished {
+                self.interactionInProgress = false
+                if self.gestureDirection == .horizontal {
+                    self.relativePosition.y = -1.0
+                } else {
+                    self.relativePosition.x = -1.0
+                }
+                self.finishTransition()
+            }
+        }
+    }
+
+    @objc private func update(_ timeInterval: TimeInterval) {
+
+        let speed = (Constants.updateFrameRate * Constants.transitionSpeedFactor)
+        let xDistance = distanceToMove.x / speed
+        let yDistance = distanceToMove.y / speed
+        distanceToMove.x -= xDistance
+        distanceToMove.y -= yDistance
+        relativePosition.x += xDistance
+        relativePosition.y += yDistance
+        updateTransition()
+
+        let translation = CGPoint(
+            x: xDistance * (UIScreen.main.bounds.size.width),
+            y: yDistance * (UIScreen.main.bounds.size.height)
+        )
+        let directionalTranslation = (gestureDirection == .horizontal) ? translation.y : translation.x
+        if abs(directionalTranslation) < 1.0 {
+
+            relativePosition.x += distanceToMove.x
+            relativePosition.y += distanceToMove.y
+            updateTransition()
+            interactionInProgress = false
+
+            finishTransition()
+        }
+    }
+
+    internal func beginTransition() {
+
+        shouldZoomOutOnInteraction = false
+        if let viewController = viewController {
+            shouldZoomOutOnInteraction = viewController.dataSource?.targetFrameForDismissal(viewController) != nil
+        }
+
+        createTransitionViews()
+
+        viewController?.mediaContainerView.isHidden = true
+        viewController?.hideControls = true
+    }
+
+    private func finishTransition() {
+
+        distanceToMove = .zero
+        timer?.invalidate()
+        timer = nil
+
+        imageView.removeFromSuperview()
+
+        let directionalPosition = (gestureDirection == .horizontal) ? relativePosition.y : relativePosition.x
+        if directionalPosition != 0.0 {
+            viewController?.dismiss(animated: false, completion: nil)
+        } else {
+            viewController?.mediaContainerView.isHidden = false
+            viewController?.hideControls = false
+        }
+    }
+
+    private func createTransitionViews() {
+
+        imageView.image = image
+        imageView.frame = imageViewFrame(
+            for: image?.size ?? .zero,
+            in: viewController?.view.bounds ?? .zero
+        )
+        viewController?.view.addSubview(imageView)
+        imageView.transform = CGAffineTransform.identity
+    }
+
+    private func updateTransition() {
+
+        var transform = CGAffineTransform.identity
+        let directionalPosition = (gestureDirection == .horizontal) ? relativePosition.y : relativePosition.x
+
+        if shouldZoomOutOnInteraction {
+            let scale = CGFloat.maximum(Constants.minimumZoomDuringInteraction, 1.0 - abs(directionalPosition))
+            transform = transform.scaledBy(x: scale, y: scale)
+        }
+
+        if gestureDirection == .horizontal {
+            transform = transform.translatedBy(
+                x: shouldZoomOutOnInteraction ? relativePosition.x * UIScreen.main.bounds.size.width : 0.0,
+                y: relativePosition.y * UIScreen.main.bounds.size.height
+            )
+        } else {
+            transform = transform.translatedBy(
+                x: relativePosition.x * UIScreen.main.bounds.size.width,
+                y: shouldZoomOutOnInteraction ? relativePosition.y * UIScreen.main.bounds.size.height : 0.0
+            )
+        }
+        imageView.transform = transform
+    }
+
+    private func imageViewFrame(for imageSize: CGSize, in frame: CGRect, mode: UIView.ContentMode = .scaleAspectFit) -> CGRect {
+
+        guard imageSize != .zero,
+            mode == .scaleAspectFit || mode == .scaleAspectFill else {
+            return frame
+        }
+
+        var targetImageSize = frame.size
+
+        let aspectHeight = frame.size.width / imageSize.width * imageSize.height
+        let aspectWidth = frame.size.height / imageSize.height * imageSize.width
+
+        if imageSize.width / imageSize.height > frame.size.width / frame.size.height {
+            if mode == .scaleAspectFit {
+                targetImageSize.height = aspectHeight
+            } else {
+                targetImageSize.width = aspectWidth
+            }
+        } else {
+            if mode == .scaleAspectFit {
+                targetImageSize.width = aspectWidth
+            } else {
+                targetImageSize.height = aspectHeight
+            }
+        }
+
+        let x = frame.minX + (frame.size.width - targetImageSize.width) / 2.0
+        let y = frame.minY + (frame.size.height - targetImageSize.height) / 2.0
+
+        return CGRect(origin: CGPoint(x: x, y: y), size: targetImageSize)
+    }
+}

+ 905 - 0
iOSClient/Viewer/NCMediaBrowser/MediaBrowserViewController.swift

@@ -0,0 +1,905 @@
+//
+//  MediaBrowserViewController.swift
+//  ATGMediaBrowser
+//
+//  Created by Suraj Thomas K on 7/10/18.
+//  Copyright © 2018 Al Tayer Group LLC.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+//  and associated documentation files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use, copy, modify, merge, publish,
+//  distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in all copies or
+//  substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+//  BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+//  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+// MARK: - MediaBrowserViewControllerDataSource protocol
+/// Protocol to supply media browser contents.
+public protocol MediaBrowserViewControllerDataSource: class {
+
+    /**
+     Completion block for passing requested media image with details.
+     - parameter index: Index of the requested media.
+     - parameter image: Image to be passed back to media browser.
+     - parameter zoomScale: Zoom scale to be applied to the image including min and max levels.
+     - parameter error: Error received while fetching the media image.
+
+     - note:
+        Remember to pass the index received in the datasource method back.
+        This index is used to set the image to the correct image view.
+     */
+    typealias CompletionBlock = (_ index: Int, _ image: UIImage?, _ zoomScale: ZoomScale?, _ error: Error?) -> Void
+
+    /**
+     Method to supply number of items to be shown in media browser.
+     - parameter mediaBrowser: Reference to media browser object.
+     - returns: An integer with number of items to be shown in media browser.
+     */
+    func numberOfItems(in mediaBrowser: MediaBrowserViewController) -> Int
+
+    /**
+     Method to supply image for specific index.
+     - parameter mediaBrowser: Reference to media browser object.
+     - parameter index: Index of the requested media.
+     - parameter completion: Completion block to be executed on fetching the media image.
+     */
+    func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, imageAt index: Int, completion: @escaping CompletionBlock)
+
+    /**
+     This method is used to get the target frame into which the browser will perform the dismiss transition.
+     - parameter mediaBrowser: Reference to media browser object.
+
+     - note:
+        If this method is not implemented, the media browser will perform slide up/down transition on dismissal.
+    */
+    func targetFrameForDismissal(_ mediaBrowser: MediaBrowserViewController) -> CGRect?
+}
+
+extension MediaBrowserViewControllerDataSource {
+
+    public func targetFrameForDismissal(_ mediaBrowser: MediaBrowserViewController) -> CGRect? { return nil }
+}
+
+// MARK: - MediaBrowserViewControllerDelegate protocol
+
+public protocol MediaBrowserViewControllerDelegate: class {
+
+    /**
+     Method invoked on scrolling to next/previous media items.
+     - parameter mediaBrowser: Reference to media browser object.
+     - parameter index: Index of the newly focussed media item.
+     - note:
+        This method will not be called on first load, and will be called only on swiping left and right.
+     */
+    func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, didChangeFocusTo index: Int)
+}
+
+extension MediaBrowserViewControllerDelegate {
+
+    public func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, didChangeFocusTo index: Int) {}
+}
+
+public class MediaBrowserViewController: UIViewController {
+
+    // MARK: - Exposed Enumerations
+
+    /**
+     Enum to hold supported gesture directions.
+
+     ```
+     case horizontal
+     case vertical
+     ```
+    */
+    public enum GestureDirection {
+
+        /// Horizontal (left - right) gestures.
+        case horizontal
+        /// Vertical (up - down) gestures.
+        case vertical
+    }
+
+    /**
+     Enum to hold supported browser styles.
+
+     ```
+     case linear
+     case carousel
+     ```
+     */
+    public enum BrowserStyle {
+
+        /// Linear browser with *0* as first index and *numItems-1* as last index.
+        case linear
+        /// Carousel browser. The media items are repeated in a circular fashion.
+        case carousel
+    }
+
+    /**
+     Enum to hold supported content draw orders.
+
+     ```
+     case previousToNext
+     case nextToPrevious
+     ```
+     - note:
+        Remember that this is draw order, not positioning. This order decides which item will
+     be above or below other items, when they overlap.
+     */
+    public enum ContentDrawOrder {
+
+        /// In this mode, media items are rendered in [previous]-[current]-[next] order.
+        case previousToNext
+        /// In this mode, media items are rendered in [next]-[current]-[previous] order.
+        case nextToPrevious
+    }
+
+    /**
+     Struct to hold support for customize title style
+
+     ```
+     font
+     textColor
+     ```
+    */
+    public struct TitleStyle {
+
+        /// Title style font
+        public var font: UIFont = UIFont.preferredFont(forTextStyle: .subheadline)
+        /// Title style text color.
+        public var textColor: UIColor = .white
+    }
+
+    // MARK: - Exposed variables
+
+    /// Data-source object to supply media browser contents.
+    public weak var dataSource: MediaBrowserViewControllerDataSource?
+    /// Delegate object to get callbacks on media browser events.
+    public weak var delegate: MediaBrowserViewControllerDelegate?
+
+    /// Gesture direction. Default is `horizontal`.
+    public var gestureDirection: GestureDirection = .horizontal
+    /// Content transformer closure. Default is `horizontalMoveInOut`.
+    public var contentTransformer: ContentTransformer = DefaultContentTransformers.horizontalMoveInOut {
+        didSet {
+
+            MediaContentView.contentTransformer = contentTransformer
+            contentViews.forEach({ $0.updateTransform() })
+        }
+    }
+    /// Content draw order. Default is `previousToNext`.
+    public var drawOrder: ContentDrawOrder = .previousToNext {
+        didSet {
+            if oldValue != drawOrder {
+                mediaContainerView.exchangeSubview(at: 0, withSubviewAt: 2)
+            }
+        }
+    }
+    /// Browser style. Default is carousel.
+    public var browserStyle: BrowserStyle = .carousel
+    /// Gap between consecutive media items. Default is `50.0`.
+    public var gapBetweenMediaViews: CGFloat = Constants.gapBetweenContents {
+        didSet {
+            MediaContentView.interItemSpacing = gapBetweenMediaViews
+            contentViews.forEach({ $0.updateTransform() })
+        }
+    }
+    /// Variable to set title style in media browser.
+    public var titleStyle: TitleStyle = TitleStyle() {
+        didSet {
+            configureTitleLabel()
+        }
+    }
+    /// Variable to set title in media browser
+    public override var title: String? {
+        didSet {
+            titleLabel.text = title
+        }
+    }
+    /// Variable to hide/show title control in media browser. Default is false.
+    public var shouldShowTitle: Bool = false {
+        didSet {
+            titleLabel.isHidden = !shouldShowTitle
+        }
+    }
+    /// Variable to hide/show page control in media browser.
+    public var shouldShowPageControl: Bool = true {
+        didSet {
+            pageControl.isHidden = !shouldShowPageControl
+        }
+    }
+    /// Variable to hide/show controls(close & page control). Default is false.
+    public var hideControls: Bool = false {
+        didSet {
+            hideControlViews(hideControls)
+        }
+    }
+    /**
+    Variable to schedule/cancel auto-hide controls(close & page control). Default is false.
+    Default delay is `3.0` seconds.
+    - todo: Update to accept auto-hide-delay.
+     */
+    public var autoHideControls: Bool = false {
+        didSet {
+            if autoHideControls {
+                DispatchQueue.main.asyncAfter(
+                    deadline: .now() + Constants.controlHideDelay,
+                    execute: controlToggleTask
+                )
+            } else {
+                controlToggleTask.cancel()
+            }
+        }
+    }
+    /// Enable or disable interactive dismissal. Default is enabled.
+    public var enableInteractiveDismissal: Bool = true
+    /// Item index of the current item. In range `0..<numMediaItems`
+    public var currentItemIndex: Int {
+
+        return sanitizeIndex(index)
+    }
+    
+    // MARK: - Private Enumerations
+
+    private enum Constants {
+
+        static let gapBetweenContents: CGFloat = 50.0
+        static let minimumVelocity: CGFloat = 15.0
+        static let minimumTranslation: CGFloat = 0.1
+        static let animationDuration = 0.3
+        static let updateFrameRate: CGFloat = 60.0
+        static let bounceFactor: CGFloat = 0.1
+        static let controlHideDelay = 3.0
+
+        enum Close {
+
+            static let top: CGFloat = 8.0
+            static let trailing: CGFloat = -8.0
+            static let height: CGFloat = 30.0
+            static let minWidth: CGFloat = 30.0
+            static let contentInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0)
+            static let borderWidth: CGFloat = 2.0
+            static let borderColor: UIColor = .white
+            static let title = "Close"
+        }
+
+        enum PageControl {
+
+            static let bottom: CGFloat = -10.0
+            static let tintColor: UIColor = .lightGray
+            static let selectedTintColor: UIColor = .white
+        }
+
+        enum Title {
+            static let top: CGFloat = 16.0
+            static let rect: CGRect = CGRect(x: 0, y: 0, width: 30, height: 30)
+        }
+    }
+
+    // MARK: - Private variables
+    private(set) var index: Int = 0 {
+        didSet {
+            pageControl.currentPage = index
+        }
+    }
+
+    private var contentViews: [MediaContentView] = []
+
+    private var controlViews: [UIView] = []
+    lazy private var controlToggleTask: DispatchWorkItem = { [unowned self] in
+
+        let item = DispatchWorkItem {
+            self.hideControls = true
+        }
+        return item
+    }()
+    lazy private var tapGestureRecognizer: UITapGestureRecognizer = { [unowned self] in
+        let gesture = UITapGestureRecognizer()
+        gesture.numberOfTapsRequired = 1
+        gesture.numberOfTouchesRequired = 1
+        gesture.delegate = self
+        gesture.addTarget(self, action: #selector(tapGestureEvent(_:)))
+        return gesture
+    }()
+
+    private var previousTranslation: CGPoint = .zero
+
+    private var timer: Timer?
+    private var distanceToMove: CGFloat = 0.0
+
+    lazy private var panGestureRecognizer: UIPanGestureRecognizer = { [unowned self] in
+        let gesture = UIPanGestureRecognizer()
+        gesture.minimumNumberOfTouches = 1
+        gesture.maximumNumberOfTouches = 1
+        gesture.delegate = self
+        gesture.addTarget(self, action: #selector(panGestureEvent(_:)))
+        return gesture
+    }()
+
+    lazy internal private(set) var mediaContainerView: UIView = { [unowned self] in
+        let container = UIView()
+        container.backgroundColor = .clear
+        return container
+    }()
+
+    lazy private var pageControl: UIPageControl = { [unowned self] in
+        let pageControl = UIPageControl()
+        pageControl.hidesForSinglePage = true
+        pageControl.numberOfPages = numMediaItems
+        pageControl.currentPageIndicatorTintColor = Constants.PageControl.selectedTintColor
+        pageControl.tintColor = Constants.PageControl.tintColor
+        pageControl.currentPage = index
+        return pageControl
+    }()
+
+    lazy var titleLabel: UILabel = {
+        let label = UILabel(frame: Constants.Title.rect)
+        label.font = self.titleStyle.font
+        label.textColor = self.titleStyle.textColor
+        label.textAlignment = .center
+        return label
+    }()
+
+    private var numMediaItems = 0
+
+    private lazy var dismissController = DismissAnimationController(
+        gestureDirection: gestureDirection,
+        viewController: self
+    )
+
+    // MARK: - Public methods
+
+    /// Invoking this method reloads the contents media browser.
+    public func reloadContentViews() {
+
+        numMediaItems = dataSource?.numberOfItems(in: self) ?? 0
+        if shouldShowPageControl {
+            pageControl.numberOfPages = numMediaItems
+        }
+
+        for contentView in contentViews {
+
+            updateContents(of: contentView)
+        }
+    }
+
+    // MARK: - Initializers
+
+    public init(
+        index: Int = 0,
+        dataSource: MediaBrowserViewControllerDataSource,
+        delegate: MediaBrowserViewControllerDelegate? = nil
+        ) {
+
+        self.index = index
+        self.dataSource = dataSource
+        self.delegate = delegate
+
+        super.init(nibName: nil, bundle: nil)
+
+        initialize()
+    }
+
+    public required init?(coder aDecoder: NSCoder) {
+
+        super.init(coder: aDecoder)
+
+        initialize()
+    }
+
+    private func initialize() {
+
+        view.backgroundColor = .clear
+
+        modalPresentationStyle = .custom
+
+        modalTransitionStyle = .crossDissolve
+    }
+}
+
+// MARK: - View Lifecycle and Events
+
+extension MediaBrowserViewController {
+
+    override public var prefersStatusBarHidden: Bool {
+
+        return true
+    }
+
+    override public func viewDidLoad() {
+
+        super.viewDidLoad()
+
+        numMediaItems = dataSource?.numberOfItems(in: self) ?? 0
+
+        populateContentViews()
+
+        addPageControl()
+
+        addTitleLabel()
+
+        view.addGestureRecognizer(panGestureRecognizer)
+        view.addGestureRecognizer(tapGestureRecognizer)
+    }
+
+    override public func viewDidAppear(_ animated: Bool) {
+
+        super.viewDidAppear(animated)
+
+        contentViews.forEach({ $0.updateTransform() })
+    }
+
+    override public func viewWillDisappear(_ animated: Bool) {
+
+        super.viewWillDisappear(animated)
+
+        if !controlToggleTask.isCancelled {
+            controlToggleTask.cancel()
+        }
+    }
+
+    public override func viewWillTransition(
+        to size: CGSize,
+        with coordinator: UIViewControllerTransitionCoordinator
+        ) {
+
+        coordinator.animate(alongsideTransition: { context in
+            self.contentViews.forEach({ $0.handleChangeInViewSize(to: size) })
+        }, completion: nil)
+
+        super.viewWillTransition(to: size, with: coordinator)
+    }
+
+    private func populateContentViews() {
+
+        view.addSubview(mediaContainerView)
+        mediaContainerView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            mediaContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            mediaContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            mediaContainerView.topAnchor.constraint(equalTo: view.topAnchor),
+            mediaContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+        ])
+
+        MediaContentView.interItemSpacing = gapBetweenMediaViews
+        MediaContentView.contentTransformer = contentTransformer
+
+        contentViews.forEach({ $0.removeFromSuperview() })
+        contentViews.removeAll()
+
+        for i in -1...1 {
+            let mediaView = MediaContentView(
+                index: i + index,
+                position: CGFloat(i),
+                frame: view.bounds
+            )
+            mediaContainerView.addSubview(mediaView)
+            mediaView.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                mediaView.leadingAnchor.constraint(equalTo: mediaContainerView.leadingAnchor),
+                mediaView.trailingAnchor.constraint(equalTo: mediaContainerView.trailingAnchor),
+                mediaView.topAnchor.constraint(equalTo: mediaContainerView.topAnchor),
+                mediaView.bottomAnchor.constraint(equalTo: mediaContainerView.bottomAnchor)
+            ])
+
+            contentViews.append(mediaView)
+
+            if numMediaItems > 0 {
+                updateContents(of: mediaView)
+            }
+        }
+        if drawOrder == .nextToPrevious {
+            mediaContainerView.exchangeSubview(at: 0, withSubviewAt: 2)
+        }
+    }
+
+    private func addPageControl() {
+
+        view.addSubview(pageControl)
+        pageControl.translatesAutoresizingMaskIntoConstraints = false
+        var bottomAnchor = view.bottomAnchor
+        if #available(iOS 11.0, *) {
+            if view.responds(to: #selector(getter: UIView.safeAreaLayoutGuide)) {
+                bottomAnchor = view.safeAreaLayoutGuide.bottomAnchor
+            }
+        }
+        NSLayoutConstraint.activate([
+            pageControl.bottomAnchor.constraint(equalTo: bottomAnchor, constant: Constants.PageControl.bottom),
+            pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor)
+        ])
+
+        controlViews.append(pageControl)
+    }
+
+    private func addTitleLabel() {
+
+        view.addSubview(titleLabel)
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
+        var topAnchor = view.topAnchor
+        if #available(iOS 11.0, *) {
+            if view.responds(to: #selector(getter: UIView.safeAreaLayoutGuide)) {
+                topAnchor = view.safeAreaLayoutGuide.topAnchor
+            }
+        }
+        NSLayoutConstraint.activate([
+            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: Constants.Title.top),
+            titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
+            ])
+
+        controlViews.append(titleLabel)
+    }
+
+    private func configureTitleLabel() {
+
+        titleLabel.font = self.titleStyle.font
+        titleLabel.textColor = self.titleStyle.textColor
+    }
+
+    private func hideControlViews(_ hide: Bool) {
+
+        self.controlViews.forEach { $0.alpha = hide ? 0.0 : 1.0 }
+        /*
+        UIView.animate(
+            withDuration: Constants.animationDuration,
+            delay: 0.0,
+            options: .beginFromCurrentState,
+            animations: {
+                self.controlViews.forEach { $0.alpha = hide ? 0.0 : 1.0 }
+            },
+            completion: nil
+        )
+        */
+    }
+
+    @objc private func didTapOnClose(_ sender: UIButton) {
+
+        if let targetFrame = dataSource?.targetFrameForDismissal(self) {
+            dismissController.image = sourceImage()
+            dismissController.beginTransition()
+            dismissController.animateToTargetFrame(targetFrame)
+        } else {
+            dismiss(animated: true, completion: nil)
+        }
+    }
+}
+
+// MARK: - Gesture Recognizers
+
+extension MediaBrowserViewController {
+
+    @objc private func panGestureEvent(_ recognizer: UIPanGestureRecognizer) {
+
+        if dismissController.interactionInProgress {
+            dismissController.handleInteractiveTransition(recognizer)
+            return
+        }
+
+        guard numMediaItems > 0 else {
+            return
+        }
+
+        let translation = recognizer.translation(in: view)
+
+        switch recognizer.state {
+        case .began:
+            previousTranslation = translation
+            distanceToMove = 0.0
+            timer?.invalidate()
+            timer = nil
+        case .changed:
+            moveViews(by: CGPoint(x: translation.x - previousTranslation.x, y: translation.y - previousTranslation.y))
+        case .ended, .failed, .cancelled:
+            let velocity = recognizer.velocity(in: view)
+
+            var viewsCopy = contentViews
+            let previousView = viewsCopy.removeFirst()
+            let middleView = viewsCopy.removeFirst()
+            let nextView = viewsCopy.removeFirst()
+
+            var toMove: CGFloat = 0.0
+            let directionalVelocity = gestureDirection == .horizontal ? velocity.x : velocity.y
+
+            if abs(directionalVelocity) < Constants.minimumVelocity &&
+                abs(middleView.position) < Constants.minimumTranslation {
+                toMove = -middleView.position
+            } else if directionalVelocity < 0.0 {
+                if middleView.position >= 0.0 {
+                    toMove = -middleView.position
+                } else {
+                    toMove = -nextView.position
+                }
+            } else {
+                if middleView.position <= 0.0 {
+                    toMove = -middleView.position
+                } else {
+                    toMove = -previousView.position
+                }
+            }
+
+            if browserStyle == .linear || numMediaItems <= 1 {
+                if (middleView.index == 0 && ((middleView.position + toMove) > 0.0)) ||
+                    (middleView.index == (numMediaItems - 1) && (middleView.position + toMove) < 0.0) {
+
+                    toMove = -middleView.position
+                }
+            }
+
+            distanceToMove = toMove
+
+            if timer == nil {
+                timer = Timer.scheduledTimer(
+                    timeInterval: 1.0/Double(Constants.updateFrameRate),
+                    target: self,
+                    selector: #selector(update(_:)),
+                    userInfo: nil,
+                    repeats: true
+                )
+            }
+        default:
+            break
+        }
+
+        previousTranslation = translation
+    }
+
+    @objc private func tapGestureEvent(_ recognizer: UITapGestureRecognizer) {
+
+        guard !dismissController.interactionInProgress else {
+            return
+        }
+
+        if !controlToggleTask.isCancelled {
+            controlToggleTask.cancel()
+        }
+        hideControls = !hideControls
+    }
+}
+
+// MARK: - Updating View Positions
+
+extension MediaBrowserViewController {
+
+    @objc private func update(_ timeInterval: TimeInterval) {
+
+        guard distanceToMove != 0.0 else {
+
+            timer?.invalidate()
+            timer = nil
+            return
+        }
+
+        let distance = distanceToMove / (Constants.updateFrameRate * 0.1)
+        distanceToMove -= distance
+        moveViewsNormalized(by: CGPoint(x: distance, y: distance))
+
+        let translation = CGPoint(
+            x: distance * (view.frame.size.width + gapBetweenMediaViews),
+            y: distance * (view.frame.size.height + gapBetweenMediaViews)
+        )
+        let directionalTranslation = (gestureDirection == .horizontal) ? translation.x : translation.y
+        if abs(directionalTranslation) < 0.1 {
+
+            moveViewsNormalized(by: CGPoint(x: distanceToMove, y: distanceToMove))
+            distanceToMove = 0.0
+            timer?.invalidate()
+            timer = nil
+        }
+    }
+
+    private func moveViews(by translation: CGPoint) {
+
+        let viewSizeIncludingGap = CGSize(
+            width: view.frame.size.width + gapBetweenMediaViews,
+            height: view.frame.size.height + gapBetweenMediaViews
+        )
+
+        let normalizedTranslation = calculateNormalizedTranslation(
+            translation: translation,
+            viewSize: viewSizeIncludingGap
+        )
+
+        moveViewsNormalized(by: normalizedTranslation)
+    }
+
+    private func moveViewsNormalized(by normalizedTranslation: CGPoint) {
+
+        let isGestureHorizontal = (gestureDirection == .horizontal)
+
+        contentViews.forEach({
+            $0.position += isGestureHorizontal ? normalizedTranslation.x : normalizedTranslation.y
+        })
+
+        var viewsCopy = contentViews
+        let previousView = viewsCopy.removeFirst()
+        let middleView = viewsCopy.removeFirst()
+        let nextView = viewsCopy.removeFirst()
+
+        let viewSizeIncludingGap = CGSize(
+            width: view.frame.size.width + gapBetweenMediaViews,
+            height: view.frame.size.height + gapBetweenMediaViews
+        )
+
+        let viewSize = isGestureHorizontal ? viewSizeIncludingGap.width : viewSizeIncludingGap.height
+        let normalizedGap = gapBetweenMediaViews/viewSize
+        let normalizedCenter = (middleView.frame.size.width / viewSize) * 0.5
+        let viewCount = contentViews.count
+
+        if middleView.position < -(normalizedGap + normalizedCenter) {
+
+            index = sanitizeIndex(index + 1)
+
+            // Previous item is taken and placed on right/down most side
+            previousView.position += CGFloat(viewCount)
+            previousView.index += viewCount
+            updateContents(of: previousView)
+
+            contentViews.removeFirst()
+            contentViews.append(previousView)
+
+            switch drawOrder {
+            case .previousToNext:
+                mediaContainerView.bringSubviewToFront(previousView)
+            case .nextToPrevious:
+                mediaContainerView.sendSubviewToBack(previousView)
+            }
+
+            delegate?.mediaBrowser(self, didChangeFocusTo: index)
+
+        } else if middleView.position > (1 + normalizedGap - normalizedCenter) {
+
+            index = sanitizeIndex(index - 1)
+
+            // Next item is taken and placed on left/top most side
+            nextView.position -= CGFloat(viewCount)
+            nextView.index -= viewCount
+            updateContents(of: nextView)
+
+            contentViews.removeLast()
+            contentViews.insert(nextView, at: 0)
+
+            switch drawOrder {
+            case .previousToNext:
+                mediaContainerView.sendSubviewToBack(nextView)
+            case .nextToPrevious:
+                mediaContainerView.bringSubviewToFront(nextView)
+            }
+
+            delegate?.mediaBrowser(self, didChangeFocusTo: index)
+        }
+    }
+
+    private func calculateNormalizedTranslation(translation: CGPoint, viewSize: CGSize) -> CGPoint {
+
+        guard let middleView = mediaView(at: 1) else {
+            return .zero
+        }
+
+        var normalizedTranslation = CGPoint(
+            x: (translation.x)/viewSize.width,
+            y: (translation.y)/viewSize.height
+        )
+
+        if browserStyle != .carousel || numMediaItems <= 1 {
+            let isGestureHorizontal = (gestureDirection == .horizontal)
+            let directionalTranslation = isGestureHorizontal ? normalizedTranslation.x : normalizedTranslation.y
+            if (middleView.index == 0 && ((middleView.position + directionalTranslation) > 0.0)) ||
+                (middleView.index == (numMediaItems - 1) && (middleView.position + directionalTranslation) < 0.0) {
+                if isGestureHorizontal {
+                    normalizedTranslation.x *= Constants.bounceFactor
+                } else {
+                    normalizedTranslation.y *= Constants.bounceFactor
+                }
+            }
+        }
+        return normalizedTranslation
+    }
+
+    private func updateContents(of contentView: MediaContentView) {
+
+        contentView.image = nil
+        let convertedIndex = sanitizeIndex(contentView.index)
+        contentView.isLoading = true
+        dataSource?.mediaBrowser(
+            self,
+            imageAt: convertedIndex,
+            completion: { [weak self] (index, image, zoom, _) in
+
+                guard let strongSelf = self else {
+                    return
+                }
+
+                if index == strongSelf.sanitizeIndex(contentView.index) {
+                    if image != nil {
+                        contentView.image = image
+                        contentView.zoomLevels = zoom
+                    }
+                    contentView.isLoading = false
+                }
+            }
+        )
+    }
+
+    private func sanitizeIndex(_ index: Int) -> Int {
+
+        let newIndex = index % numMediaItems
+        if newIndex < 0 {
+            return newIndex + numMediaItems
+        }
+        return newIndex
+    }
+
+    private func sourceImage() -> UIImage? {
+
+        return mediaView(at: 1)?.image
+    }
+
+    private func mediaView(at index: Int) -> MediaContentView? {
+
+        guard index < contentViews.count else {
+
+            assertionFailure("Content views does not have this many views. : \(index)")
+            return nil
+        }
+        return contentViews[index]
+    }
+}
+
+// MARK: - UIGestureRecognizerDelegate
+
+extension MediaBrowserViewController: UIGestureRecognizerDelegate {
+
+    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+
+        guard enableInteractiveDismissal else {
+            return true
+        }
+
+        let middleView = mediaView(at: 1)
+        if middleView?.zoomScale == middleView?.zoomLevels?.minimumZoomScale,
+            let recognizer = gestureRecognizer as? UIPanGestureRecognizer {
+
+            let translation = recognizer.translation(in: recognizer.view)
+
+            if gestureDirection == .horizontal {
+                dismissController.interactionInProgress = abs(translation.y) > abs(translation.x)
+            } else {
+                dismissController.interactionInProgress = abs(translation.x) > abs(translation.y)
+            }
+            if dismissController.interactionInProgress {
+                dismissController.image = sourceImage()
+            }
+        }
+        return true
+    }
+
+    public func gestureRecognizer(
+        _ gestureRecognizer: UIGestureRecognizer,
+        shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer
+        ) -> Bool {
+
+        if gestureRecognizer is UIPanGestureRecognizer,
+            let scrollView = otherGestureRecognizer.view as? MediaContentView {
+            return scrollView.zoomScale == 1.0
+        }
+        return false
+    }
+
+    public func gestureRecognizer(
+        _ gestureRecognizer: UIGestureRecognizer,
+        shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
+        ) -> Bool {
+
+        if gestureRecognizer is UITapGestureRecognizer {
+            return otherGestureRecognizer.view is MediaContentView
+        }
+        return false
+    }
+}

+ 313 - 0
iOSClient/Viewer/NCMediaBrowser/MediaContentView.swift

@@ -0,0 +1,313 @@
+//
+//  MediaContentView.swift
+//  ATGMediaBrowser
+//
+//  Created by Suraj Thomas K on 7/10/18.
+//  Copyright © 2018 Al Tayer Group LLC.
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+//  and associated documentation files (the "Software"), to deal in the Software without
+//  restriction, including without limitation the rights to use, copy, modify, merge, publish,
+//  distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+//  Software is furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in all copies or
+//  substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+//  BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+//  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+/// Holds the value of minimumZoomScale and maximumZoomScale of the image.
+public struct ZoomScale {
+
+    /// Minimum zoom level, the image can be zoomed out to.
+    public var minimumZoomScale: CGFloat
+
+    /// Maximum zoom level, the image can be zoomed into.
+    public var maximumZoomScale: CGFloat
+
+    /// Default zoom scale. minimum is 1.0 and maximum is 3.0
+    public static let `default` = ZoomScale(
+        minimum: 1.0,
+        maximum: 3.0
+    )
+
+    /// Identity zoom scale. Pass this to disable zoom.
+    public static let identity = ZoomScale(
+        minimum: 1.0,
+        maximum: 1.0
+    )
+
+    /**
+     Initializer.
+     - parameter minimum: The minimum zoom level.
+     - parameter maximum: The maximum zoom level.
+     */
+    public init(minimum: CGFloat, maximum: CGFloat) {
+
+        minimumZoomScale = minimum
+        maximumZoomScale = maximum
+    }
+}
+
+internal class MediaContentView: UIScrollView {
+
+    // MARK: - Exposed variables
+    internal static var interItemSpacing: CGFloat = 0.0
+    internal var index: Int {
+        didSet {
+            resetZoom()
+        }
+    }
+    internal static var contentTransformer: ContentTransformer = DefaultContentTransformers.horizontalMoveInOut
+
+    internal var position: CGFloat {
+        didSet {
+            updateTransform()
+        }
+    }
+    internal var image: UIImage? {
+        didSet {
+            updateImageView()
+        }
+    }
+    internal var isLoading: Bool = false {
+        didSet {
+            indicatorContainer.isHidden = !isLoading
+            if isLoading {
+                indicator.startAnimating()
+            } else {
+                indicator.stopAnimating()
+            }
+        }
+    }
+    internal var zoomLevels: ZoomScale? {
+        didSet {
+            zoomScale = ZoomScale.default.minimumZoomScale
+            minimumZoomScale = zoomLevels?.minimumZoomScale ?? ZoomScale.default.minimumZoomScale
+            maximumZoomScale = zoomLevels?.maximumZoomScale ?? ZoomScale.default.maximumZoomScale
+        }
+    }
+
+    // MARK: - Private enumerations
+
+    private enum Constants {
+
+        static let indicatorViewSize: CGFloat = 60.0
+    }
+
+    // MARK: - Private variables
+
+    private lazy var imageView: UIImageView = {
+        let imageView = UIImageView()
+        imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+        imageView.contentMode = .scaleAspectFit
+        imageView.clipsToBounds = true
+        return imageView
+    }()
+
+    private lazy var indicator: UIActivityIndicatorView = {
+        let indicatorView = UIActivityIndicatorView()
+        indicatorView.style = .whiteLarge
+        indicatorView.hidesWhenStopped = true
+        return indicatorView
+    }()
+
+    private lazy var indicatorContainer: UIView = {
+        let container = UIView()
+        container.backgroundColor = .darkGray
+        container.layer.cornerRadius = Constants.indicatorViewSize * 0.5
+        container.layer.masksToBounds = true
+        return container
+    }()
+
+    private lazy var doubleTapGestureRecognizer: UITapGestureRecognizer = { [unowned self] in
+        let gesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap(_:)))
+        gesture.numberOfTapsRequired = 2
+        gesture.numberOfTouchesRequired = 1
+        return gesture
+    }()
+
+    init(index itemIndex: Int, position: CGFloat, frame: CGRect) {
+
+        self.index = itemIndex
+        self.position = position
+
+        super.init(frame: frame)
+
+        initializeViewComponents()
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+
+        fatalError("Do nto use `init?(coder:)`")
+    }
+}
+
+// MARK: - View Composition and Events
+
+extension MediaContentView {
+
+    private func initializeViewComponents() {
+
+        addSubview(imageView)
+        imageView.frame = frame
+
+        setupIndicatorView()
+
+        configureScrollView()
+
+        addGestureRecognizer(doubleTapGestureRecognizer)
+
+        updateTransform()
+    }
+
+    private func configureScrollView() {
+
+        isMultipleTouchEnabled = true
+        showsHorizontalScrollIndicator = false
+        showsVerticalScrollIndicator = false
+        contentSize = imageView.bounds.size
+        canCancelContentTouches = false
+        zoomLevels = ZoomScale.default
+        delegate = self
+        bouncesZoom = false
+    }
+
+    private func resetZoom() {
+
+        setZoomScale(1.0, animated: false)
+        imageView.transform = CGAffineTransform.identity
+        contentSize = imageView.frame.size
+        contentOffset = .zero
+    }
+
+    private func setupIndicatorView() {
+
+        addSubview(indicatorContainer)
+        indicatorContainer.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            indicatorContainer.widthAnchor.constraint(equalToConstant: Constants.indicatorViewSize),
+            indicatorContainer.heightAnchor.constraint(equalToConstant: Constants.indicatorViewSize),
+            indicatorContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
+            indicatorContainer.centerYAnchor.constraint(equalTo: centerYAnchor)
+        ])
+
+        indicatorContainer.addSubview(indicator)
+        indicator.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            indicator.leadingAnchor.constraint(equalTo: indicatorContainer.leadingAnchor),
+            indicator.trailingAnchor.constraint(equalTo: indicatorContainer.trailingAnchor),
+            indicator.topAnchor.constraint(equalTo: indicatorContainer.topAnchor),
+            indicator.bottomAnchor.constraint(equalTo: indicatorContainer.bottomAnchor)
+        ])
+
+        indicatorContainer.setNeedsLayout()
+        indicatorContainer.layoutIfNeeded()
+
+        indicatorContainer.isHidden = true
+    }
+
+    internal func updateTransform() {
+
+        MediaContentView.contentTransformer(self, position)
+    }
+
+    internal func handleChangeInViewSize(to size: CGSize) {
+
+        let oldScale = zoomScale
+        zoomScale = 1.0
+        imageView.frame = CGRect(origin: .zero, size: size)
+
+        updateImageView()
+        updateTransform()
+        setZoomScale(oldScale, animated: false)
+
+        contentSize = imageView.frame.size
+    }
+
+    @objc private func didDoubleTap(_ recognizer: UITapGestureRecognizer) {
+
+        let locationInImage = recognizer.location(in: imageView)
+
+        let isImageCoveringScreen = imageView.frame.size.width > bounds.size.width &&
+            imageView.frame.size.height > bounds.size.height
+        let zoomTo = (isImageCoveringScreen || zoomScale == maximumZoomScale) ? minimumZoomScale : maximumZoomScale
+
+        guard zoomTo != zoomScale else {
+            return
+        }
+
+        let width = bounds.size.width / zoomTo
+        let height = bounds.size.height / zoomTo
+
+        let zoomRect = CGRect(
+            x: locationInImage.x - width * 0.5,
+            y: locationInImage.y - height * 0.5,
+            width: width,
+            height: height
+        )
+
+        zoom(to: zoomRect, animated: true)
+    }
+}
+
+// MARK: - UIScrollViewDelegate
+
+extension MediaContentView: UIScrollViewDelegate {
+
+    internal func viewForZooming(in scrollView: UIScrollView) -> UIView? {
+
+        let shouldAllowZoom = (image != nil && position == 0.0)
+        return shouldAllowZoom ? imageView : nil
+    }
+
+    internal func scrollViewDidZoom(_ scrollView: UIScrollView) {
+
+        centerImageView()
+    }
+
+    private func centerImageView() {
+
+        var imageViewFrame = imageView.frame
+
+        if imageViewFrame.size.width < bounds.size.width {
+            imageViewFrame.origin.x = (bounds.size.width - imageViewFrame.size.width) / 2.0
+        } else {
+            imageViewFrame.origin.x = 0.0
+        }
+
+        if imageViewFrame.size.height < bounds.size.height {
+            imageViewFrame.origin.y = (bounds.size.height - imageViewFrame.size.height) / 2.0
+        } else {
+            imageViewFrame.origin.y = 0.0
+        }
+
+        imageView.frame = imageViewFrame
+    }
+
+    private func updateImageView() {
+
+        imageView.image = image
+
+        if let contentImage = image {
+
+            let imageViewSize = bounds.size
+            let imageSize = contentImage.size
+            var targetImageSize = imageViewSize
+
+            if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height {
+                targetImageSize.height = imageViewSize.width / imageSize.width * imageSize.height
+            } else {
+                targetImageSize.width = imageViewSize.height / imageSize.height * imageSize.width
+            }
+
+            imageView.frame = CGRect(origin: .zero, size: targetImageSize)
+        }
+        centerImageView()
+    }
+}

BIN
iOSClient/Viewer/NCViewerPhotoGallery/image1.jpeg


BIN
iOSClient/Viewer/NCViewerPhotoGallery/image2.jpeg


BIN
iOSClient/Viewer/NCViewerPhotoGallery/image3.jpeg