Add fullscreen image previewing and zooming
This commit is contained in:
parent
5ed508b709
commit
3ee0506b4a
@ -56,6 +56,8 @@
|
|||||||
513C5D0D232574DA003D4054 /* RSTree.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9520DD8CFE00CA8CF5 /* RSTree.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
513C5D0D232574DA003D4054 /* RSTree.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9520DD8CFE00CA8CF5 /* RSTree.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
513C5D0E232574E4003D4054 /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; };
|
513C5D0E232574E4003D4054 /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; };
|
||||||
513C5D0F232574E4003D4054 /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
513C5D0F232574E4003D4054 /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
|
5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5142192923522B5500E07E2C /* ImageViewController.swift */; };
|
||||||
|
514219372352510100E07E2C /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514219362352510100E07E2C /* ImageScrollView.swift */; };
|
||||||
5144EA2F2279FAB600D19003 /* AccountsDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */; };
|
5144EA2F2279FAB600D19003 /* AccountsDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */; };
|
||||||
5144EA362279FC3D00D19003 /* AccountsAddLocal.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */; };
|
5144EA362279FC3D00D19003 /* AccountsAddLocal.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */; };
|
||||||
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA372279FC6200D19003 /* AccountsAddLocalWindowController.swift */; };
|
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA372279FC6200D19003 /* AccountsAddLocalWindowController.swift */; };
|
||||||
@ -750,6 +752,8 @@
|
|||||||
513C5CE8232571C2003D4054 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
513C5CE8232571C2003D4054 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||||
513C5CEB232571C2003D4054 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
513C5CEB232571C2003D4054 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||||
513C5CED232571C2003D4054 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
513C5CED232571C2003D4054 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
5142192923522B5500E07E2C /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
|
||||||
|
514219362352510100E07E2C /* ImageScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageScrollView.swift; sourceTree = "<group>"; };
|
||||||
5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsDetailViewController.swift; sourceTree = "<group>"; };
|
5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsDetailViewController.swift; sourceTree = "<group>"; };
|
||||||
5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsAddLocal.xib; sourceTree = "<group>"; };
|
5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsAddLocal.xib; sourceTree = "<group>"; };
|
||||||
5144EA372279FC6200D19003 /* AccountsAddLocalWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddLocalWindowController.swift; sourceTree = "<group>"; };
|
5144EA372279FC6200D19003 /* AccountsAddLocalWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddLocalWindowController.swift; sourceTree = "<group>"; };
|
||||||
@ -1359,6 +1363,8 @@
|
|||||||
51C4527E2265092C00C03939 /* ArticleViewController.swift */,
|
51C4527E2265092C00C03939 /* ArticleViewController.swift */,
|
||||||
517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */,
|
517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */,
|
||||||
51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */,
|
51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */,
|
||||||
|
5142192923522B5500E07E2C /* ImageViewController.swift */,
|
||||||
|
514219362352510100E07E2C /* ImageScrollView.swift */,
|
||||||
);
|
);
|
||||||
path = Article;
|
path = Article;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -2842,6 +2848,7 @@
|
|||||||
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
|
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
|
||||||
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */,
|
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */,
|
||||||
51C4529D22650A1000C03939 /* FaviconURLFinder.swift in Sources */,
|
51C4529D22650A1000C03939 /* FaviconURLFinder.swift in Sources */,
|
||||||
|
5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */,
|
||||||
51C45258226508CF00C03939 /* AppAssets.swift in Sources */,
|
51C45258226508CF00C03939 /* AppAssets.swift in Sources */,
|
||||||
51FA73A82332BE880090D516 /* ExtractedArticle.swift in Sources */,
|
51FA73A82332BE880090D516 /* ExtractedArticle.swift in Sources */,
|
||||||
51C4527C2265091600C03939 /* MasterTimelineDefaultCellLayout.swift in Sources */,
|
51C4527C2265091600C03939 /* MasterTimelineDefaultCellLayout.swift in Sources */,
|
||||||
@ -2874,6 +2881,7 @@
|
|||||||
5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */,
|
5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */,
|
||||||
51C452882265093600C03939 /* AddFeedViewController.swift in Sources */,
|
51C452882265093600C03939 /* AddFeedViewController.swift in Sources */,
|
||||||
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
|
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
|
||||||
|
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
|
||||||
DF999FF722B5AEFA0064B687 /* SafariView.swift in Sources */,
|
DF999FF722B5AEFA0064B687 /* SafariView.swift in Sources */,
|
||||||
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
|
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
|
||||||
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
|
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,
|
||||||
|
@ -14,6 +14,18 @@ function linkHover() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used to pop a resizable image view
|
||||||
|
function imageWasClicked(img) {
|
||||||
|
window.webkit.messageHandlers.imageWasClicked.postMessage(img.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the click listeners for images
|
||||||
|
function imageClicks() {
|
||||||
|
document.querySelectorAll("img").forEach(element => {
|
||||||
|
element.addEventListener("click", function() { imageWasClicked(this) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Here we are making iframes responsive. Particularly useful for inline Youtube videos.
|
// Here we are making iframes responsive. Particularly useful for inline Youtube videos.
|
||||||
function wrapFrames() {
|
function wrapFrames() {
|
||||||
document.querySelectorAll("iframe").forEach(element => {
|
document.querySelectorAll("iframe").forEach(element => {
|
||||||
@ -52,5 +64,6 @@ function render(data) {
|
|||||||
wrapFrames()
|
wrapFrames()
|
||||||
stripStyles()
|
stripStyles()
|
||||||
linkHover()
|
linkHover()
|
||||||
|
imageClicks()
|
||||||
inlineVideos()
|
inlineVideos()
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,10 @@ enum ArticleViewState: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ArticleViewController: UIViewController {
|
class ArticleViewController: UIViewController {
|
||||||
|
|
||||||
|
private struct MessageName {
|
||||||
|
static let imageWasClicked = "imageWasClicked"
|
||||||
|
}
|
||||||
|
|
||||||
@IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem!
|
@IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem!
|
||||||
@IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem!
|
@IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem!
|
||||||
@ -102,7 +106,10 @@ class ArticleViewController: UIViewController {
|
|||||||
self.webViewContainer.addChildAndPin(webView)
|
self.webViewContainer.addChildAndPin(webView)
|
||||||
webView.navigationDelegate = self
|
webView.navigationDelegate = self
|
||||||
webView.uiDelegate = self
|
webView.uiDelegate = self
|
||||||
|
|
||||||
|
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasClicked)
|
||||||
|
webView.configuration.userContentController.add(self, name: MessageName.imageWasClicked)
|
||||||
|
|
||||||
// Even though page.html should be loaded into this webview, we have to do it again
|
// Even though page.html should be loaded into this webview, we have to do it again
|
||||||
// to work around this bug: http://www.openradar.me/22855188
|
// to work around this bug: http://www.openradar.me/22855188
|
||||||
webView.loadHTMLString(ArticleRenderer.page.html, baseURL: ArticleRenderer.page.baseURL)
|
webView.loadHTMLString(ArticleRenderer.page.html, baseURL: ArticleRenderer.page.baseURL)
|
||||||
@ -337,6 +344,20 @@ extension ArticleViewController: WKUIDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: WKScriptMessageHandler
|
||||||
|
|
||||||
|
extension ArticleViewController: WKScriptMessageHandler {
|
||||||
|
|
||||||
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
|
if message.name == MessageName.imageWasClicked, let link = message.body as? String, let url = URL(string: link) {
|
||||||
|
let imageVC = UIStoryboard.main.instantiateController(ofType: ImageViewController.self)
|
||||||
|
imageVC.url = url
|
||||||
|
imageVC.modalPresentationStyle = .fullScreen
|
||||||
|
present(imageVC, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private extension ArticleViewController {
|
private extension ArticleViewController {
|
||||||
|
372
iOS/Article/ImageScrollView.swift
Normal file
372
iOS/Article/ImageScrollView.swift
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
//
|
||||||
|
// ImageScrollView.swift
|
||||||
|
// Beauty
|
||||||
|
//
|
||||||
|
// Created by Nguyen Cong Huy on 1/19/16.
|
||||||
|
// Copyright © 2016 Nguyen Cong Huy. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc public protocol ImageScrollViewDelegate: UIScrollViewDelegate {
|
||||||
|
func imageScrollViewDidChangeOrientation(imageScrollView: ImageScrollView)
|
||||||
|
func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView)
|
||||||
|
func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open class ImageScrollView: UIScrollView {
|
||||||
|
|
||||||
|
@objc public enum ScaleMode: Int {
|
||||||
|
case aspectFill
|
||||||
|
case aspectFit
|
||||||
|
case widthFill
|
||||||
|
case heightFill
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public enum Offset: Int {
|
||||||
|
case begining
|
||||||
|
case center
|
||||||
|
}
|
||||||
|
|
||||||
|
static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2
|
||||||
|
|
||||||
|
@objc open var imageContentMode: ScaleMode = .widthFill
|
||||||
|
@objc open var initialOffset: Offset = .begining
|
||||||
|
|
||||||
|
@objc public private(set) var zoomView: UIImageView? = nil
|
||||||
|
|
||||||
|
@objc open weak var imageScrollViewDelegate: ImageScrollViewDelegate?
|
||||||
|
|
||||||
|
var imageSize: CGSize = CGSize.zero
|
||||||
|
private var pointToCenterAfterResize: CGPoint = CGPoint.zero
|
||||||
|
private var scaleToRestoreAfterResize: CGFloat = 1.0
|
||||||
|
var maxScaleFromMinScale: CGFloat = 3.0
|
||||||
|
|
||||||
|
override open var frame: CGRect {
|
||||||
|
willSet {
|
||||||
|
if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
|
||||||
|
prepareToResize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
didSet {
|
||||||
|
if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
|
||||||
|
recoverFromResizing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize() {
|
||||||
|
showsVerticalScrollIndicator = false
|
||||||
|
showsHorizontalScrollIndicator = false
|
||||||
|
bouncesZoom = true
|
||||||
|
decelerationRate = UIScrollView.DecelerationRate.fast
|
||||||
|
delegate = self
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(ImageScrollView.changeOrientationNotification), name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func adjustFrameToCenter() {
|
||||||
|
|
||||||
|
guard let unwrappedZoomView = zoomView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var frameToCenter = unwrappedZoomView.frame
|
||||||
|
|
||||||
|
// center horizontally
|
||||||
|
if frameToCenter.size.width < bounds.width {
|
||||||
|
frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
frameToCenter.origin.x = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// center vertically
|
||||||
|
if frameToCenter.size.height < bounds.height {
|
||||||
|
frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
frameToCenter.origin.y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrappedZoomView.frame = frameToCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
private func prepareToResize() {
|
||||||
|
let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||||
|
pointToCenterAfterResize = convert(boundsCenter, to: zoomView)
|
||||||
|
|
||||||
|
scaleToRestoreAfterResize = zoomScale
|
||||||
|
|
||||||
|
// If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum
|
||||||
|
// allowable scale when the scale is restored.
|
||||||
|
if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) {
|
||||||
|
scaleToRestoreAfterResize = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func recoverFromResizing() {
|
||||||
|
setMaxMinZoomScalesForCurrentBounds()
|
||||||
|
|
||||||
|
// restore zoom scale, first making sure it is within the allowable range.
|
||||||
|
let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize)
|
||||||
|
zoomScale = min(maximumZoomScale, maxZoomScale)
|
||||||
|
|
||||||
|
// restore center point, first making sure it is within the allowable range.
|
||||||
|
|
||||||
|
// convert our desired center point back to our own coordinate space
|
||||||
|
let boundsCenter = convert(pointToCenterAfterResize, to: zoomView)
|
||||||
|
|
||||||
|
// calculate the content offset that would yield that center point
|
||||||
|
var offset = CGPoint(x: boundsCenter.x - bounds.size.width/2.0, y: boundsCenter.y - bounds.size.height/2.0)
|
||||||
|
|
||||||
|
// restore offset, adjusted to be within the allowable range
|
||||||
|
let maxOffset = maximumContentOffset()
|
||||||
|
let minOffset = minimumContentOffset()
|
||||||
|
|
||||||
|
var realMaxOffset = min(maxOffset.x, offset.x)
|
||||||
|
offset.x = max(minOffset.x, realMaxOffset)
|
||||||
|
|
||||||
|
realMaxOffset = min(maxOffset.y, offset.y)
|
||||||
|
offset.y = max(minOffset.y, realMaxOffset)
|
||||||
|
|
||||||
|
contentOffset = offset
|
||||||
|
}
|
||||||
|
|
||||||
|
private func maximumContentOffset() -> CGPoint {
|
||||||
|
return CGPoint(x: contentSize.width - bounds.width,y:contentSize.height - bounds.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func minimumContentOffset() -> CGPoint {
|
||||||
|
return CGPoint.zero
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Set up
|
||||||
|
|
||||||
|
open func setup() {
|
||||||
|
var topSupperView = superview
|
||||||
|
|
||||||
|
while topSupperView?.superview != nil {
|
||||||
|
topSupperView = topSupperView?.superview
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure views have already layout with precise frame
|
||||||
|
topSupperView?.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Display image
|
||||||
|
|
||||||
|
@objc open func display(image: UIImage) {
|
||||||
|
|
||||||
|
if let zoomView = zoomView {
|
||||||
|
zoomView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomView = UIImageView(image: image)
|
||||||
|
zoomView!.isUserInteractionEnabled = true
|
||||||
|
addSubview(zoomView!)
|
||||||
|
|
||||||
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTapGestureRecognizer(_:)))
|
||||||
|
tapGesture.numberOfTapsRequired = 2
|
||||||
|
zoomView!.addGestureRecognizer(tapGesture)
|
||||||
|
|
||||||
|
let downSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeUpGestureRecognizer(_:)))
|
||||||
|
downSwipeGesture.direction = .down
|
||||||
|
zoomView!.addGestureRecognizer(downSwipeGesture)
|
||||||
|
|
||||||
|
let upSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeDownGestureRecognizer(_:)))
|
||||||
|
upSwipeGesture.direction = .up
|
||||||
|
zoomView!.addGestureRecognizer(upSwipeGesture)
|
||||||
|
|
||||||
|
configureImageForSize(image.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureImageForSize(_ size: CGSize) {
|
||||||
|
imageSize = size
|
||||||
|
contentSize = imageSize
|
||||||
|
setMaxMinZoomScalesForCurrentBounds()
|
||||||
|
zoomScale = minimumZoomScale
|
||||||
|
|
||||||
|
switch initialOffset {
|
||||||
|
case .begining:
|
||||||
|
contentOffset = CGPoint.zero
|
||||||
|
case .center:
|
||||||
|
let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2
|
||||||
|
let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2
|
||||||
|
|
||||||
|
switch imageContentMode {
|
||||||
|
case .aspectFit:
|
||||||
|
contentOffset = CGPoint.zero
|
||||||
|
case .aspectFill:
|
||||||
|
contentOffset = CGPoint(x: xOffset, y: yOffset)
|
||||||
|
case .heightFill:
|
||||||
|
contentOffset = CGPoint(x: xOffset, y: 0)
|
||||||
|
case .widthFill:
|
||||||
|
contentOffset = CGPoint(x: 0, y: yOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setMaxMinZoomScalesForCurrentBounds() {
|
||||||
|
// calculate min/max zoomscale
|
||||||
|
let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise
|
||||||
|
let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise
|
||||||
|
|
||||||
|
var minScale: CGFloat = 1
|
||||||
|
|
||||||
|
switch imageContentMode {
|
||||||
|
case .aspectFill:
|
||||||
|
minScale = max(xScale, yScale)
|
||||||
|
case .aspectFit:
|
||||||
|
minScale = min(xScale, yScale)
|
||||||
|
case .widthFill:
|
||||||
|
minScale = xScale
|
||||||
|
case .heightFill:
|
||||||
|
minScale = yScale
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let maxScale = maxScaleFromMinScale*minScale
|
||||||
|
|
||||||
|
// don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.)
|
||||||
|
if minScale > maxScale {
|
||||||
|
minScale = maxScale
|
||||||
|
}
|
||||||
|
|
||||||
|
maximumZoomScale = maxScale
|
||||||
|
minimumZoomScale = minScale * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Gesture
|
||||||
|
|
||||||
|
@objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
|
||||||
|
// zoom out if it bigger than middle scale point. Else, zoom in
|
||||||
|
if zoomScale >= maximumZoomScale / 2.0 {
|
||||||
|
setZoomScale(minimumZoomScale, animated: true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let center = gestureRecognizer.location(in: gestureRecognizer.view)
|
||||||
|
let zoomRect = zoomRectForScale(ImageScrollView.kZoomInFactorFromMinWhenDoubleTap * minimumZoomScale, center: center)
|
||||||
|
zoom(to: zoomRect, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func swipeUpGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
|
||||||
|
if gestureRecognizer.state == .ended {
|
||||||
|
imageScrollViewDelegate?.imageScrollViewDidGestureSwipeUp(imageScrollView: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func swipeDownGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
|
||||||
|
if gestureRecognizer.state == .ended {
|
||||||
|
imageScrollViewDelegate?.imageScrollViewDidGestureSwipeDown(imageScrollView: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect {
|
||||||
|
var zoomRect = CGRect.zero
|
||||||
|
|
||||||
|
// the zoom rect is in the content view's coordinates.
|
||||||
|
// at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds.
|
||||||
|
// as the zoom scale decreases, so more content is visible, the size of the rect grows.
|
||||||
|
zoomRect.size.height = frame.size.height / scale
|
||||||
|
zoomRect.size.width = frame.size.width / scale
|
||||||
|
|
||||||
|
// choose an origin so as to get the right center.
|
||||||
|
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0)
|
||||||
|
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0)
|
||||||
|
|
||||||
|
return zoomRect
|
||||||
|
}
|
||||||
|
|
||||||
|
open func refresh() {
|
||||||
|
if let image = zoomView?.image {
|
||||||
|
display(image: image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
@objc func changeOrientationNotification() {
|
||||||
|
// A weird bug that frames are not update right after orientation changed. Need delay a little bit with async.
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.configureImageForSize(self.imageSize)
|
||||||
|
self.imageScrollViewDelegate?.imageScrollViewDidChangeOrientation(imageScrollView: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImageScrollView: UIScrollViewDelegate {
|
||||||
|
|
||||||
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
imageScrollViewDelegate?.scrollViewDidScroll?(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||||
|
imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||||
|
imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
|
||||||
|
imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||||||
|
imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 11.0, *)
|
||||||
|
public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
|
||||||
|
imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||||
|
return zoomView
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||||
|
adjustFrameToCenter()
|
||||||
|
imageScrollViewDelegate?.scrollViewDidZoom?(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
69
iOS/Article/ImageViewController.swift
Normal file
69
iOS/Article/ImageViewController.swift
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
//
|
||||||
|
// ImageViewController.swift
|
||||||
|
// NetNewsWire-iOS
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 10/12/19.
|
||||||
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ImageViewController: UIViewController {
|
||||||
|
|
||||||
|
@IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
|
||||||
|
@IBOutlet weak var imageScrollView: ImageScrollView!
|
||||||
|
|
||||||
|
private var dataTask: URLSessionDataTask? = nil
|
||||||
|
var url: URL!
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
activityIndicatorView.isHidden = false
|
||||||
|
activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
|
imageScrollView.setup()
|
||||||
|
imageScrollView.imageScrollViewDelegate = self
|
||||||
|
imageScrollView.imageContentMode = .aspectFit
|
||||||
|
imageScrollView.initialOffset = .center
|
||||||
|
|
||||||
|
dataTask = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let data = data, let image = UIImage(data: data) {
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.activityIndicatorView.isHidden = true
|
||||||
|
self.activityIndicatorView.stopAnimating()
|
||||||
|
self.imageScrollView.display(image: image)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dataTask!.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func done(_ sender: Any) {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: ImageScrollViewDelegate
|
||||||
|
|
||||||
|
extension ImageViewController: ImageScrollViewDelegate {
|
||||||
|
|
||||||
|
func imageScrollViewDidChangeOrientation(imageScrollView: ImageScrollView) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView) {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView) {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15508"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
@ -226,6 +227,61 @@
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="900" y="-759"/>
|
<point key="canvasLocation" x="900" y="-759"/>
|
||||||
</scene>
|
</scene>
|
||||||
|
<!--Image View Controller-->
|
||||||
|
<scene sceneID="TT4-oA-DBw">
|
||||||
|
<objects>
|
||||||
|
<viewController storyboardIdentifier="ImageViewController" id="vO9-a3-Dnu" customClass="ImageViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="w6Q-vH-063">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<scrollView verifyAmbiguity="off" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="msG-pz-EKk" customClass="ImageScrollView" customModule="NetNewsWire" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<subviews>
|
||||||
|
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" style="large" translatesAutoresizingMaskIntoConstraints="NO" id="iEh-n3-Vkg">
|
||||||
|
<rect key="frame" x="188.5" y="390.5" width="37" height="37"/>
|
||||||
|
</activityIndicatorView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="iEh-n3-Vkg" firstAttribute="centerX" secondItem="msG-pz-EKk" secondAttribute="centerX" id="FSP-DY-Vax"/>
|
||||||
|
<constraint firstItem="iEh-n3-Vkg" firstAttribute="centerY" secondItem="msG-pz-EKk" secondAttribute="centerY" id="rev-zC-wMY"/>
|
||||||
|
</constraints>
|
||||||
|
<viewLayoutGuide key="contentLayoutGuide" id="phv-DN-krZ"/>
|
||||||
|
<viewLayoutGuide key="frameLayoutGuide" id="NNU-C8-Fsz"/>
|
||||||
|
</scrollView>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cXR-ll-xBx">
|
||||||
|
<rect key="frame" x="0.0" y="44" width="44" height="44"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="44" id="6kc-Gw-KbZ"/>
|
||||||
|
<constraint firstAttribute="width" constant="44" id="cBq-gs-WzN"/>
|
||||||
|
</constraints>
|
||||||
|
<color key="tintColor" name="primaryAccentColor"/>
|
||||||
|
<state key="normal" image="multiply.circle.fill" catalog="system"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="done:" destination="vO9-a3-Dnu" eventType="touchUpInside" id="tgd-ov-4Ft"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="mbY-02-GFL" firstAttribute="bottom" secondItem="msG-pz-EKk" secondAttribute="bottom" id="AtA-bA-jDr"/>
|
||||||
|
<constraint firstItem="mbY-02-GFL" firstAttribute="trailing" secondItem="msG-pz-EKk" secondAttribute="trailing" id="R49-qV-8nm"/>
|
||||||
|
<constraint firstItem="msG-pz-EKk" firstAttribute="leading" secondItem="mbY-02-GFL" secondAttribute="leading" id="XN1-xN-hYS"/>
|
||||||
|
<constraint firstItem="msG-pz-EKk" firstAttribute="top" secondItem="mbY-02-GFL" secondAttribute="top" id="p1a-s0-wdK"/>
|
||||||
|
<constraint firstItem="cXR-ll-xBx" firstAttribute="leading" secondItem="mbY-02-GFL" secondAttribute="leading" id="vJs-LN-Ydd"/>
|
||||||
|
<constraint firstItem="cXR-ll-xBx" firstAttribute="top" secondItem="mbY-02-GFL" secondAttribute="top" id="xVN-Qt-WYA"/>
|
||||||
|
</constraints>
|
||||||
|
<viewLayoutGuide key="safeArea" id="mbY-02-GFL"/>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="activityIndicatorView" destination="iEh-n3-Vkg" id="xue-X1-awS"/>
|
||||||
|
<outlet property="imageScrollView" destination="msG-pz-EKk" id="dGi-M6-dcO"/>
|
||||||
|
</connections>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="ZPN-tH-JAG" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="3056.521739130435" y="-759.375"/>
|
||||||
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="chevron.down" catalog="system" width="64" height="36"/>
|
<image name="chevron.down" catalog="system" width="64" height="36"/>
|
||||||
@ -233,8 +289,12 @@
|
|||||||
<image name="chevron.up" catalog="system" width="64" height="36"/>
|
<image name="chevron.up" catalog="system" width="64" height="36"/>
|
||||||
<image name="circle" catalog="system" width="64" height="60"/>
|
<image name="circle" catalog="system" width="64" height="60"/>
|
||||||
<image name="gear" catalog="system" width="64" height="58"/>
|
<image name="gear" catalog="system" width="64" height="58"/>
|
||||||
|
<image name="multiply.circle.fill" catalog="system" width="64" height="60"/>
|
||||||
<image name="safari" catalog="system" width="64" height="60"/>
|
<image name="safari" catalog="system" width="64" height="60"/>
|
||||||
<image name="square.and.arrow.up" catalog="system" width="56" height="64"/>
|
<image name="square.and.arrow.up" catalog="system" width="56" height="64"/>
|
||||||
<image name="star" catalog="system" width="64" height="58"/>
|
<image name="star" catalog="system" width="64" height="58"/>
|
||||||
|
<namedColor name="primaryAccentColor">
|
||||||
|
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user