Fix lint issues.

This commit is contained in:
Brent Simmons
2025-01-22 22:18:09 -08:00
parent 40ada2ba5a
commit bbef99f2d3
92 changed files with 1651 additions and 1694 deletions

View File

@@ -16,9 +16,9 @@ enum ArticleExtractorButtonState {
}
class ArticleExtractorButton: UIButton {
private var animatedLayer: CALayer?
var buttonState: ArticleExtractorButtonState = .off {
didSet {
if buttonState != oldValue {
@@ -39,7 +39,7 @@ class ArticleExtractorButton: UIButton {
}
}
}
override var accessibilityLabel: String? {
get {
switch buttonState {
@@ -57,7 +57,7 @@ class ArticleExtractorButton: UIButton {
super.accessibilityLabel = newValue
}
}
override func layoutSubviews() {
super.layoutSubviews()
guard case .animated = buttonState else {
@@ -66,31 +66,31 @@ class ArticleExtractorButton: UIButton {
stripAnimatedSublayer()
addAnimatedSublayer(to: layer)
}
private func stripAnimatedSublayer() {
animatedLayer?.removeFromSuperlayer()
}
private func addAnimatedSublayer(to hostedLayer: CALayer) {
let image1 = AppAssets.articleExtractorOffTinted.cgImage!
let image2 = AppAssets.articleExtractorOnTinted.cgImage!
let images = [image1, image2, image1]
animatedLayer = CALayer()
let imageSize = AppAssets.articleExtractorOff.size
animatedLayer!.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)
animatedLayer!.position = CGPoint(x: bounds.midX, y: bounds.midY)
hostedLayer.addSublayer(animatedLayer!)
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = CAAnimationCalculationMode.linear
animation.keyTimes = [0, 0.5, 1]
animation.duration = 2
animation.values = images as [Any]
animation.repeatCount = HUGE
animatedLayer!.add(animation, forKey: "contents")
}
}

View File

@@ -15,15 +15,14 @@ import UIKit
@objc optional func searchBar(_ searchBar: ArticleSearchBar, textDidChange: String)
}
@IBDesignable final class ArticleSearchBar: UIStackView {
var searchField: UISearchTextField!
var nextButton: UIButton!
var prevButton: UIButton!
var background: UIView!
weak private var resultsLabel: UILabel!
var resultsCount: UInt = 0 {
didSet {
updateUI()
@@ -34,30 +33,30 @@ import UIKit
updateUI()
}
}
weak var delegate: SearchBarDelegate?
override var keyCommands: [UIKeyCommand]? {
return [UIKeyCommand(title: "Exit Find", action: #selector(donePressed(_:)), input: UIKeyCommand.inputEscape)]
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
layer.backgroundColor = UIColor(named: "barBackgroundColor")?.cgColor ?? UIColor.white.cgColor
isOpaque = true
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: searchField)
}
private func updateUI() {
if resultsCount > 0 {
let format = NSLocalizedString("%d of %d", comment: "Results selection and count")
@@ -65,23 +64,23 @@ import UIKit
} else {
resultsLabel.text = NSLocalizedString("No results", comment: "No results")
}
nextButton.isEnabled = selectedResult < resultsCount
prevButton.isEnabled = resultsCount > 0 && selectedResult > 1
}
@discardableResult override func becomeFirstResponder() -> Bool {
searchField.becomeFirstResponder()
}
@discardableResult override func resignFirstResponder() -> Bool {
searchField.resignFirstResponder()
}
override var isFirstResponder: Bool {
searchField.isFirstResponder
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@@ -94,12 +93,12 @@ private extension ArticleSearchBar {
spacing = 8
layoutMargins.left = 8
layoutMargins.right = 8
background = UIView(frame: bounds)
background.backgroundColor = .systemGray5
background.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(background)
let doneButton = UIButton()
doneButton.setTitle(NSLocalizedString("Done", comment: "Done"), for: .normal)
doneButton.setTitleColor(UIColor.label, for: .normal)
@@ -108,14 +107,14 @@ private extension ArticleSearchBar {
doneButton.addTarget(self, action: #selector(donePressed), for: .touchUpInside)
doneButton.isEnabled = true
addArrangedSubview(doneButton)
let resultsLabel = UILabel()
searchField = UISearchTextField()
searchField.autocapitalizationType = .none
searchField.autocorrectionType = .no
searchField.returnKeyType = .search
searchField.delegate = self
resultsLabel.font = .systemFont(ofSize: UIFont.smallSystemFontSize)
resultsLabel.textColor = .secondaryLabel
resultsLabel.text = ""
@@ -123,17 +122,17 @@ private extension ArticleSearchBar {
resultsLabel.adjustsFontSizeToFitWidth = true
searchField.rightView = resultsLabel
searchField.rightViewMode = .always
self.resultsLabel = resultsLabel
addArrangedSubview(searchField)
prevButton = UIButton(type: .system)
prevButton.setImage(UIImage(systemName: "chevron.up"), for: .normal)
prevButton.accessibilityLabel = "Previous Result"
prevButton.isAccessibilityElement = true
prevButton.addTarget(self, action: #selector(previousPressed), for: .touchUpInside)
addArrangedSubview(prevButton)
nextButton = UIButton(type: .system)
nextButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
nextButton.accessibilityLabel = "Next Result"
@@ -144,25 +143,25 @@ private extension ArticleSearchBar {
}
private extension ArticleSearchBar {
@objc func textDidChange(_ notification: Notification) {
delegate?.searchBar?(self, textDidChange: searchField.text ?? "")
if searchField.text?.isEmpty ?? true {
searchField.rightViewMode = .never
} else {
searchField.rightViewMode = .always
}
}
@objc func nextPressed() {
delegate?.nextWasPressed?(self)
}
@objc func previousPressed() {
delegate?.previousWasPressed?(self)
}
@objc func donePressed(_ _: Any? = nil) {
delegate?.doneWasPressed?(self)
}

View File

@@ -16,21 +16,21 @@ final class ContextMenuPreviewViewController: UIViewController {
@IBOutlet weak var blogAuthorLabel: UILabel!
@IBOutlet weak var articleTitleLabel: UILabel!
@IBOutlet weak var dateTimeLabel: UILabel!
var article: Article?
init(article: Article?) {
self.article = article
super.init(nibName: "ContextMenuPreviewViewController", bundle: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
blogNameLabel.text = article?.feed?.nameForDisplay ?? ""
blogAuthorLabel.text = article?.byline()
articleTitleLabel.text = article?.title ?? ""
@@ -39,14 +39,14 @@ final class ContextMenuPreviewViewController: UIViewController {
icon.iconImage = article?.iconImage()
icon.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(icon)
NSLayoutConstraint.activate([
icon.widthAnchor.constraint(equalToConstant: 48),
icon.heightAnchor.constraint(equalToConstant: 48),
icon.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
icon.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20)
])
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .medium
@@ -57,7 +57,7 @@ final class ContextMenuPreviewViewController: UIViewController {
// When in landscape the context menu preview will force this controller into a tiny
// view space. If it is documented anywhere what that is, I haven't found it. This
// set of magic numbers is what I worked out by testing a variety of phones.
let width: CGFloat
let heightPadding: CGFloat
if view.bounds.width > view.bounds.height {
@@ -68,7 +68,7 @@ final class ContextMenuPreviewViewController: UIViewController {
width = view.bounds.width
heightPadding = 8
}
view.setNeedsLayout()
view.layoutIfNeeded()
preferredContentSize = CGSize(width: width, height: dateTimeLabel.frame.maxY + heightPadding)

View File

@@ -12,27 +12,27 @@ class FindInArticleActivity: UIActivity {
override var activityTitle: String? {
NSLocalizedString("Find in Article", comment: "Find in Article")
}
override var activityType: UIActivity.ActivityType? {
UIActivity.ActivityType(rawValue: "com.ranchero.NetNewsWire.find")
}
override var activityImage: UIImage? {
UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))
}
override class var activityCategory: UIActivity.Category {
.action
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
true
}
override func prepare(withActivityItems activityItems: [Any]) {
}
override func perform() {
NotificationCenter.default.post(Notification(name: .FindInArticle))
activityDidFinish(true)

View File

@@ -14,63 +14,63 @@ import UIKit
}
open class ImageScrollView: UIScrollView {
@objc public enum ScaleMode: Int {
case aspectFill
case aspectFit
case widthFill
case heightFill
}
@objc public enum Offset: Int {
case beginning
case center
}
static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2
@objc open var imageContentMode: ScaleMode = .widthFill
@objc open var initialOffset: Offset = .beginning
@objc public private(set) var zoomView: UIImageView? = nil
@objc public private(set) var zoomView: UIImageView?
@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
var zoomedFrame: CGRect {
return zoomView?.frame ?? CGRect.zero
}
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()
}
private func initialize() {
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
@@ -78,135 +78,135 @@ open class ImageScrollView: UIScrollView {
decelerationRate = UIScrollView.DecelerationRate.fast
delegate = self
}
@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)
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)
adjustFrameToCenter()
}
private func configureImageForSize(_ size: CGSize) {
imageSize = size
contentSize = imageSize
setMaxMinZoomScalesForCurrentBounds()
zoomScale = minimumZoomScale
switch initialOffset {
case .beginning:
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
@@ -219,14 +219,14 @@ open class ImageScrollView: UIScrollView {
}
}
}
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)
@@ -237,21 +237,20 @@ open class ImageScrollView: UIScrollView {
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 {
@@ -262,96 +261,96 @@ open class ImageScrollView: UIScrollView {
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)
}
}
open func resize() {
self.configureImageForSize(self.imageSize)
}
}
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
}
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)

View File

@@ -16,15 +16,15 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
var originFrame: CGRect!
var maskFrame: CGRect!
var originImage: UIImage!
init(controller: WebViewController) {
self.webViewController = controller
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if presenting {
animateTransitionPresenting(using: transitionContext)
@@ -32,23 +32,23 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
animateTransitionReturning(using: transitionContext)
}
}
private func animateTransitionPresenting(using transitionContext: UIViewControllerContextTransitioning) {
let imageView = UIImageView(image: originImage)
imageView.frame = originFrame
let fromView = transitionContext.view(forKey: .from)!
fromView.removeFromSuperview()
transitionContext.containerView.backgroundColor = AppAssets.fullScreenBackgroundColor
transitionContext.containerView.addSubview(imageView)
webViewController?.hideClickedImage()
UIView.animate(
withDuration: duration,
delay:0.0,
delay: 0.0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2,
animations: {
@@ -61,40 +61,40 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
transitionContext.completeTransition(true)
})
}
private func animateTransitionReturning(using transitionContext: UIViewControllerContextTransitioning) {
let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController
let imageView = UIImageView(image: originImage)
imageView.frame = imageController.zoomedFrame
let fromView = transitionContext.view(forKey: .from)!
let windowFrame = fromView.window!.frame
fromView.removeFromSuperview()
let toView = transitionContext.view(forKey: .to)!
transitionContext.containerView.addSubview(toView)
let maskingView = UIView()
let fullMaskFrame = CGRect(x: windowFrame.minX, y: maskFrame.minY, width: windowFrame.width, height: maskFrame.height)
let path = UIBezierPath(rect: fullMaskFrame)
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
maskingView.layer.mask = maskLayer
maskingView.addSubview(imageView)
transitionContext.containerView.addSubview(maskingView)
UIView.animate(
withDuration: duration,
delay:0.0,
delay: 0.0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2,
animations: {
imageView.frame = self.originFrame
}, completion: { _ in
if let controller = self.webViewController {
controller.showClickedImage() {
controller.showClickedImage {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
imageView.removeFromSuperview()
transitionContext.completeTransition(true)
@@ -106,5 +106,5 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
}
})
}
}

View File

@@ -17,7 +17,7 @@ final class ImageViewController: UIViewController {
@IBOutlet weak var titleBackground: UIVisualEffectView!
@IBOutlet weak var titleLeading: NSLayoutConstraint!
@IBOutlet weak var titleTrailing: NSLayoutConstraint!
var image: UIImage!
var imageTitle: String?
var zoomedFrame: CGRect {
@@ -27,27 +27,27 @@ final class ImageViewController: UIViewController {
init() {
super.init(nibName: "ImageViewController", bundle: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
closeButton.imageView?.contentMode = .scaleAspectFit
closeButton.accessibilityLabel = NSLocalizedString("Close", comment: "Close")
shareButton.accessibilityLabel = NSLocalizedString("Share", comment: "Share")
imageScrollView.setup()
imageScrollView.imageScrollViewDelegate = self
imageScrollView.imageContentMode = .aspectFit
imageScrollView.initialOffset = .center
imageScrollView.display(image: image)
titleLabel.text = imageTitle ?? ""
layoutTitleLabel()
guard imageTitle != "" else {
titleBackground.removeFromSuperview()
return
@@ -57,11 +57,11 @@ final class ImageViewController: UIViewController {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [weak self] context in
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.imageScrollView.resize()
})
}
@IBAction func share(_ sender: Any) {
guard let image = image else { return }
let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
@@ -69,12 +69,12 @@ final class ImageViewController: UIViewController {
activityViewController.popoverPresentationController?.sourceRect = shareButton.bounds
present(activityViewController, animated: true)
}
@IBAction func done(_ sender: Any) {
dismiss(animated: true)
}
private func layoutTitleLabel(){
private func layoutTitleLabel() {
let width = view.frame.width
let multiplier = UIDevice.current.userInterfaceIdiom == .pad ? CGFloat(0.1) : CGFloat(0.04)
titleLeading.constant += width * multiplier
@@ -90,10 +90,9 @@ extension ImageViewController: ImageScrollViewDelegate {
func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView) {
dismiss(animated: true)
}
func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView) {
dismiss(animated: true)
}
}

View File

@@ -9,17 +9,17 @@
import UIKit
class OpenInBrowserActivity: UIActivity {
private var activityItems: [Any]?
override var activityTitle: String? {
return NSLocalizedString("Open in Browser", comment: "Open in Browser")
}
override var activityImage: UIImage? {
return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))
}
override var activityType: UIActivity.ActivityType? {
return UIActivity.ActivityType(rawValue: "com.rancharo.NetNewsWire-Evergreen.safari")
}
@@ -27,23 +27,23 @@ class OpenInBrowserActivity: UIActivity {
override class var activityCategory: UIActivity.Category {
return .action
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
return true
}
override func prepare(withActivityItems activityItems: [Any]) {
self.activityItems = activityItems
}
override func perform() {
guard let url = activityItems?.first(where: { $0 is URL }) as? URL else {
activityDidFinish(false)
return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
activityDidFinish(true)
}
}

View File

@@ -30,7 +30,7 @@ final class WebViewController: UIViewController {
private var bottomShowBarsView: UIView!
private var topShowBarsViewConstraint: NSLayoutConstraint!
private var bottomShowBarsViewConstraint: NSLayoutConstraint!
var webView: WKWebView? {
return view.subviews[0] as? WKWebView
}
@@ -43,7 +43,7 @@ final class WebViewController: UIViewController {
private lazy var transition = ImageTransition(controller: self)
private var clickedImageCompletion: (() -> Void)?
private var articleExtractor: ArticleExtractor? = nil
private var articleExtractor: ArticleExtractor?
var extractedArticle: ExtractedArticle? {
didSet {
windowScrollY = 0
@@ -56,12 +56,12 @@ final class WebViewController: UIViewController {
delegate?.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState)
}
}
weak var coordinator: SceneCoordinator!
weak var delegate: WebViewControllerDelegate?
private(set) var article: Article?
let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3)
var windowScrollY = 0
private var restoreWindowScrollY: Int?
@@ -77,13 +77,13 @@ final class WebViewController: UIViewController {
// Configure the tap zones
configureTopShowBarsView()
configureBottomShowBarsView()
loadWebView()
}
// MARK: Notifications
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
reloadArticleImage()
}
@@ -101,16 +101,16 @@ final class WebViewController: UIViewController {
}
// MARK: Actions
@objc func showBars(_ sender: Any) {
showBars()
}
// MARK: API
func setArticle(_ article: Article?, updateView: Bool = true) {
stopArticleExtractor()
if article != self.article {
self.article = article
if updateView {
@@ -121,9 +121,9 @@ final class WebViewController: UIViewController {
loadWebView()
}
}
}
func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) {
if isShowingExtractedArticle {
switch articleExtractor?.state {
@@ -144,7 +144,7 @@ final class WebViewController: UIViewController {
loadWebView()
}
}
func focus() {
webView?.becomeFirstResponder()
}
@@ -164,7 +164,7 @@ final class WebViewController: UIViewController {
let overlap = 2 * UIFont.systemFont(ofSize: UIFont.systemFontSize).lineHeight * UIScreen.main.scale
let scrollToY: CGFloat = {
let scrollDistance = webView.scrollView.layoutMarginsGuide.layoutFrame.height - overlap;
let scrollDistance = webView.scrollView.layoutMarginsGuide.layoutFrame.height - overlap
let fullScroll = webView.scrollView.contentOffset.y + (scrollingUp ? -scrollDistance : scrollDistance)
let final = finalScrollPosition(scrollingUp: scrollingUp)
return (scrollingUp ? fullScroll > final : fullScroll < final) ? fullScroll : final
@@ -186,12 +186,12 @@ final class WebViewController: UIViewController {
func hideClickedImage() {
webView?.evaluateJavaScript("hideClickedImage();")
}
func showClickedImage(completion: @escaping () -> Void) {
clickedImageCompletion = completion
webView?.evaluateJavaScript("showClickedImage();")
}
func fullReload() {
loadWebView(replaceExistingWebView: true)
}
@@ -205,7 +205,7 @@ final class WebViewController: UIViewController {
navigationController?.setToolbarHidden(false, animated: true)
configureContextMenuInteraction()
}
func hideBars() {
if isFullScreenAvailable {
AppDefaults.shared.articleFullscreenEnabled = true
@@ -248,7 +248,7 @@ final class WebViewController: UIViewController {
}
}
func stopArticleExtractorIfProcessing() {
if articleExtractor?.state == .processing {
stopArticleExtractor()
@@ -261,7 +261,7 @@ final class WebViewController: UIViewController {
cancelImageLoad(webView)
}
}
func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) {
guard let url = article?.preferredURL else { return }
let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()])
@@ -325,12 +325,12 @@ extension WebViewController: ArticleExtractorDelegate {
extension WebViewController: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: contextMenuPreviewProvider) { [weak self] suggestedActions in
return UIContextMenuConfiguration(identifier: nil, previewProvider: contextMenuPreviewProvider) { [weak self] _ in
guard let self = self else { return nil }
var menus = [UIMenu]()
var navActions = [UIAction]()
if let action = self.prevArticleAction() {
navActions.append(action)
@@ -341,7 +341,7 @@ extension WebViewController: UIContextMenuInteractionDelegate {
if !navActions.isEmpty {
menus.append(UIMenu(title: "", options: .displayInline, children: navActions))
}
var toggleActions = [UIAction]()
if let action = self.toggleReadAction() {
toggleActions.append(action)
@@ -355,29 +355,29 @@ extension WebViewController: UIContextMenuInteractionDelegate {
menus.append(UIMenu(title: "", options: .displayInline, children: [self.toggleArticleExtractorAction()]))
menus.append(UIMenu(title: "", options: .displayInline, children: [self.shareAction()]))
return UIMenu(title: "", children: menus)
}
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
coordinator.showBrowserForCurrentArticle()
}
}
// MARK: WKNavigationDelegate
extension WebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if components?.scheme == "http" || components?.scheme == "https" {
decisionHandler(.cancel)
@@ -391,16 +391,16 @@ extension WebViewController: WKNavigationDelegate {
self.openURLInSafariViewController(url)
}
}
} else if components?.scheme == "mailto" {
decisionHandler(.cancel)
guard let emailAddress = url.percentEncodedEmailAddress else {
return
}
if UIApplication.shared.canOpenURL(emailAddress) {
UIApplication.shared.open(emailAddress, options: [.universalLinksOnly : false], completionHandler: nil)
UIApplication.shared.open(emailAddress, options: [.universalLinksOnly: false], completionHandler: nil)
} else {
let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), message: NSLocalizedString("This device cannot send emails.", comment: "This device cannot send emails."), preferredStyle: .alert)
alert.addAction(.init(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil))
@@ -408,11 +408,11 @@ extension WebViewController: WKNavigationDelegate {
}
} else if components?.scheme == "tel" {
decisionHandler(.cancel)
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil)
UIApplication.shared.open(url, options: [.universalLinksOnly: false], completionHandler: nil)
}
} else {
decisionHandler(.allow)
}
@@ -424,13 +424,13 @@ extension WebViewController: WKNavigationDelegate {
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
fullReload()
}
}
// MARK: WKUIDelegate
extension WebViewController: WKUIDelegate {
func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) {
// We need to have at least an unimplemented WKUIDelegate assigned to the WKWebView. This makes the
// link preview launch Safari when the link preview is tapped. In theory, you should be able to get
@@ -442,11 +442,11 @@ extension WebViewController: WKUIDelegate {
guard let url = navigationAction.request.url else {
return nil
}
openURL(url)
return nil
}
}
// MARK: WKScriptMessageHandler
@@ -467,7 +467,7 @@ extension WebViewController: WKScriptMessageHandler {
return
}
}
}
// MARK: UIViewControllerTransitioningDelegate
@@ -478,7 +478,7 @@ extension WebViewController: UIViewControllerTransitioningDelegate {
transition.presenting = true
return transition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.presenting = false
return transition
@@ -488,11 +488,11 @@ extension WebViewController: UIViewControllerTransitioningDelegate {
// MARK:
extension WebViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
}
@objc func scrollPositionDidChange() {
webView?.evaluateJavaScript("window.scrollY") { (scrollY, error) in
guard error == nil else { return }
@@ -502,11 +502,9 @@ extension WebViewController: UIScrollViewDelegate {
self.windowScrollY = javascriptScrollY
}
}
}
// MARK: JSON
private struct ImageClickMessage: Codable {
@@ -564,7 +562,7 @@ private extension WebViewController {
func renderPage(_ webView: WKWebView?) {
guard let webView = webView else { return }
let theme = ArticleThemesManager.shared.currentTheme
let rendering: ArticleRenderer.Rendering
@@ -583,7 +581,7 @@ private extension WebViewController {
} else {
rendering = ArticleRenderer.noSelectionHTML(theme: theme)
}
let substitutions = [
"title": rendering.title,
"baseURL": rendering.baseURL,
@@ -595,7 +593,7 @@ private extension WebViewController {
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL))
}
func finalScrollPosition(scrollingUp: Bool) -> CGFloat {
guard let webView = webView else { return 0 }
@@ -605,7 +603,7 @@ private extension WebViewController {
return webView.scrollView.contentSize.height - webView.scrollView.bounds.height + webView.scrollView.safeAreaInsets.bottom
}
}
func startArticleExtractor() {
guard articleExtractor == nil else { return }
if let link = article?.preferredLink, let extractor = ArticleExtractor(link) {
@@ -629,12 +627,12 @@ private extension WebViewController {
var components = URLComponents()
components.scheme = ArticleRenderer.imageIconScheme
components.path = article.articleID
if let imageSrc = components.string {
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
}
}
func imageWasClicked(body: String?) {
guard let webView = webView,
let body = body,
@@ -642,22 +640,22 @@ private extension WebViewController {
let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data),
let range = clickMessage.imageURL.range(of: ";base64,")
else { return }
let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound))
if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) {
let y = CGFloat(clickMessage.y) + webView.safeAreaInsets.top
let rect = CGRect(x: CGFloat(clickMessage.x), y: y, width: CGFloat(clickMessage.width), height: CGFloat(clickMessage.height))
transition.originFrame = webView.convert(rect, to: nil)
if navigationController?.navigationBar.isHidden ?? false {
transition.maskFrame = webView.convert(webView.frame, to: nil)
} else {
transition.maskFrame = webView.convert(webView.safeAreaLayoutGuide.layoutFrame, to: nil)
}
transition.originImage = image
coordinator.showFullScreenImage(image: image, imageTitle: clickMessage.imageTitle, transitioningDelegate: self)
}
}
@@ -675,13 +673,13 @@ private extension WebViewController {
topShowBarsView.backgroundColor = .clear
topShowBarsView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(topShowBarsView)
if AppDefaults.shared.logicalArticleFullscreenEnabled {
topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: -44.0)
} else {
topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: 0.0)
}
NSLayoutConstraint.activate([
topShowBarsViewConstraint,
view.leadingAnchor.constraint(equalTo: topShowBarsView.leadingAnchor),
@@ -690,7 +688,7 @@ private extension WebViewController {
])
topShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:))))
}
func configureBottomShowBarsView() {
bottomShowBarsView = UIView()
topShowBarsView.backgroundColor = .clear
@@ -709,7 +707,7 @@ private extension WebViewController {
])
bottomShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:))))
}
func configureContextMenuInteraction() {
if isFullScreenAvailable {
if navigationController?.isNavigationBarHidden ?? false {
@@ -719,33 +717,33 @@ private extension WebViewController {
}
}
}
func contextMenuPreviewProvider() -> UIViewController {
ContextMenuPreviewViewController(article: article)
}
func prevArticleAction() -> UIAction? {
guard coordinator.isPrevArticleAvailable else { return nil }
let title = NSLocalizedString("Previous Article", comment: "Previous Article")
return UIAction(title: title, image: AppAssets.prevArticleImage) { [weak self] action in
return UIAction(title: title, image: AppAssets.prevArticleImage) { [weak self] _ in
self?.coordinator.selectPrevArticle()
}
}
func nextArticleAction() -> UIAction? {
guard coordinator.isNextArticleAvailable else { return nil }
let title = NSLocalizedString("Next Article", comment: "Next Article")
return UIAction(title: title, image: AppAssets.nextArticleImage) { [weak self] action in
return UIAction(title: title, image: AppAssets.nextArticleImage) { [weak self] _ in
self?.coordinator.selectNextArticle()
}
}
func toggleReadAction() -> UIAction? {
guard let article = article, !article.status.read || article.isAvailableToMarkUnread else { return nil }
let title = article.status.read ? NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Read", comment: "Mark as Read")
let readImage = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage
return UIAction(title: title, image: readImage) { [weak self] action in
return UIAction(title: title, image: readImage) { [weak self] _ in
self?.coordinator.toggleReadForCurrentArticle()
}
}
@@ -754,7 +752,7 @@ private extension WebViewController {
let starred = article?.status.starred ?? false
let title = starred ? NSLocalizedString("Mark as Unstarred", comment: "Mark as Unstarred") : NSLocalizedString("Mark as Starred", comment: "Mark as Starred")
let starredImage = starred ? AppAssets.starOpenImage : AppAssets.starClosedImage
return UIAction(title: title, image: starredImage) { [weak self] action in
return UIAction(title: title, image: starredImage) { [weak self] _ in
self?.coordinator.toggleStarredForCurrentArticle()
}
}
@@ -762,23 +760,23 @@ private extension WebViewController {
func nextUnreadArticleAction() -> UIAction? {
guard coordinator.isAnyUnreadAvailable else { return nil }
let title = NSLocalizedString("Next Unread Article", comment: "Next Unread Article")
return UIAction(title: title, image: AppAssets.nextUnreadArticleImage) { [weak self] action in
return UIAction(title: title, image: AppAssets.nextUnreadArticleImage) { [weak self] _ in
self?.coordinator.selectNextUnread()
}
}
func toggleArticleExtractorAction() -> UIAction {
let extracted = articleExtractorButtonState == .on
let title = extracted ? NSLocalizedString("Show Feed Article", comment: "Show Feed Article") : NSLocalizedString("Show Reader View", comment: "Show Reader View")
let extractorImage = extracted ? AppAssets.articleExtractorOffSF : AppAssets.articleExtractorOnSF
return UIAction(title: title, image: extractorImage) { [weak self] action in
return UIAction(title: title, image: extractorImage) { [weak self] _ in
self?.toggleArticleExtractor()
}
}
func shareAction() -> UIAction {
let title = NSLocalizedString("Share", comment: "Share")
return UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in
return UIAction(title: title, image: AppAssets.shareImage) { [weak self] _ in
self?.showActivityDialog()
}
}
@@ -816,27 +814,27 @@ internal struct FindInArticleState: Codable {
let width: Double
let height: Double
}
struct FindInArticleResult: Codable {
let rects: [WebViewClientRect]
let bounds: WebViewClientRect
let index: UInt
let matchGroups: [String]
}
let index: UInt?
let results: [FindInArticleResult]
let count: UInt
}
extension WebViewController {
func searchText(_ searchText: String, completionHandler: @escaping (FindInArticleState) -> Void) {
guard let json = try? JSONEncoder().encode(FindInArticleOptions(text: searchText)) else {
return
}
let encoded = json.base64EncodedString()
webView?.evaluateJavaScript("updateFind(\"\(encoded)\")") {
(result, error) in
guard error == nil,
@@ -845,21 +843,21 @@ extension WebViewController {
let findState = try? JSONDecoder().decode(FindInArticleState.self, from: rawData) else {
return
}
completionHandler(findState)
}
}
func endSearch() {
webView?.evaluateJavaScript("endFind()")
}
func selectNextSearchResult() {
webView?.evaluateJavaScript("selectNextResult()")
}
func selectPreviousSearchResult() {
webView?.evaluateJavaScript("selectPreviousResult()")
}
}

View File

@@ -10,16 +10,16 @@ import Foundation
import WebKit
class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler {
// We need to wrap a message handler to prevent a circlular reference
private weak var handler: WKScriptMessageHandler?
init(_ handler: WKScriptMessageHandler) {
self.handler = handler
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
handler?.userContentController(userContentController, didReceive: message)
}
}