我的编程空间,编程开发者的网络收藏夹
学习永远不晚

如何利用SwiftUI实现可缩放的图片预览器

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何利用SwiftUI实现可缩放的图片预览器

前言

在开发中,我们经常会遇到点击图片查看大图的需求。在 Apple 的推动下,iOS 开发必定会从 UIKit 慢慢向 SwiftUI 转变。为了更好地适应这一趋势,今天我们用 SwiftUI 实现一个可缩放的图片预览器。

实现过程

程序的初步构想

要做一个程序,首先肯定是给它起个名字。既然是图片预览器(Image Previewer),再加上我自己习惯用的前缀 LBJ,就把它命名为 LBJImagePreviewer 吧。

既然是图片预览器,所以需要外部提供图片给我们;然后是可缩放,所以需要一个最大的缩放倍数。有了这些思考,可以把 LBJImagePreviewer 简单定义为:


import SwiftUI

public struct LBJImagePreviewer: View {

  private let uiImage: UIImage
  private let maxScale: CGFloat

  public init(uiImage: UIImage, maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale) {
    self.uiImage = uiImage
    self.maxScale = maxScale
  }

  public var body: some View {
    EmptyView()
  }
}

public enum LBJImagePreviewerConstants {
  public static let defaultMaxScale: CGFloat = 16
}

在上面代码中,给 maxScale 设置了一个默认值。

另外还可以看到 maxScale 的默认值是通过 LBJImagePreviewerConstants.defaultMaxScale 来设置的,而不是直接写 16,这样做的目的是把代码中用到的数值和经验值等整理到一个地方,方便后续的修改。这是一个好的编程习惯。

细心的读者可能还会注意到 LBJImagePreviewerConstants 是一个 enum 类型。为什么不用 struct 或者 class 呢?

在 Swift 中定义静态方法,class / struct / enum 三者如何选择?

在开发过程中,我们经常会遇到需要定义一些静态方法的需求。通常我们会想到用 class 和 struct 去定义,然而却忽略了 enum 也可以拥有静态方法。那么问题来了:既然三者都可以定义静态方法,那么我们应该如何选择?
下面直接给出答案:

  • class:class 是引用类型,支持继承。如果你需要这两个特性,那么选择 class。
  • struct:struct 是值类型,不支持继承。如果你需要值类型,并且某些时候需要这个类型的实例,那么用 struct。
  • enum:enum 也是值类型,一般用来定义一组相关的值。如果我们想要的静态方法是一系列的工具,不需要任何的实例化和继承,那么用 enum 最合适。

另外,其实这个规则也适用于静态变量。

显示 UIImage

当用户点开图片预览器,当然是希望图片等比例占据整个图片预览器,所以需要知道图片预览器当前的尺寸和图片尺寸,从而通过计算让图片等比例占据整个图片预览器。

图片预览器当前的尺寸可以通过 GeometryReader 得到;图片大小可以直接从 UIImage 得到。所以我们可以把

LBJImagePreviewer 的 body 定义如下:


public struct LBJImagePreviewer: View {
  public var body: some View {
    GeometryReader { geometry in                  // 用于获取图片预览器所占据的尺寸
      let imageSize = imageSize(fits: geometry)   // 计算图片等比例铺满整个预览器时的尺寸
      ScrollView([.vertical, .horizontal]) {
        imageContent
          .frame(
            width: imageSize.width,
            height: imageSize.height
          )
          .padding(.vertical, (max(0, geometry.size.height - imageSize.height) / 2))  // 让图片在预览器垂直方向上居中
      }
      .background(Color.black)
    }
    .ignoresSafeArea()
  }
}

private extension LBJImagePreviewer {
  var imageContent: some View {
    Image(uiImage: uiImage)
      .resizable()
      .aspectRatio(contentMode: .fit)
  }

  /// 计算图片等比例铺满整个预览器时的尺寸
  func imageSize(fits geometry: GeometryProxy) -> CGSize {
      let hZoom = geometry.size.width / uiImage.size.width
      let vZoom = geometry.size.height / uiImage.size.height
      return uiImage.size * min(hZoom, vZoom)
  }
}

extension CGSize {
  /// CGSize 乘以 CGFloat
  static func * (lhs: Self, rhs: CGFloat) -> CGSize {
    CGSize(width: lhs.width * rhs, height: lhs.height * rhs)
  }
}

这样我们就把图片用 ScrollView 显示出来了。

双击缩放

想要 ScrollView 的内容可以滚动起来,必须要让它的尺寸大于 ScrollView 的尺寸。沿着这个思路可以想到,我们可修改 imageContent 的大小来实现放大缩小,也就是修改下面这个 frame:


imageContent
  .frame(
    width: imageSize.width,
    height: imageSize.height
  )

我们通过用 imageSize(fits: geometry) 的返回值乘以一个倍数,就可以改变 frame 的大小。这个倍数就是放大的倍数。因此我们定义一个变量记录倍数,然后通过双击手势改变它,就能把图片放大缩小,有变动的代码如下:


// 当前的放大倍数
@State
private var zoomScale: CGFloat = 1

public var body: some View {
  GeometryReader { geometry in
    let zoomedImageSize = zoomedImageSize(fits: geometry)
    ScrollView([.vertical, .horizontal]) {
      imageContent
        .gesture(doubleTapGesture())
        .frame(
          width: zoomedImageSize.width,
          height: zoomedImageSize.height
        )
        .padding(.vertical, (max(0, geometry.size.height - zoomedImageSize.height) / 2))
    }
    .background(Color.black)
  }
  .ignoresSafeArea()
}

// 双击手势
func doubleTapGesture() -> some Gesture {
  TapGesture(count: 2)
    .onEnded {
      withAnimation {
        if zoomScale > 1 {
          zoomScale = 1
        } else {
          zoomScale = maxScale
        }
      }
    }
}

// 缩放时图片的大小
func zoomedImageSize(fits geometry: GeometryProxy) -> CGSize {
  imageSize(fits: geometry) * zoomScale
}

放大手势缩放

放大手势缩放的原理与双击一样,都是想办法通过修改 zoomScale 来达到缩放图片的目的。SwiftUI 中的放大手势是 MagnificationGesture。代码变动如下:


// 稳定的放大倍数,放大手势以此为基准来改变 zoomScale 的值
@State
private var steadyStateZoomScale: CGFloat = 1

// 放大手势缩放过程中产生的倍数变化
@GestureState
private var gestureZoomScale: CGFloat = 1

// 变成了只读属性,当前图片被放大的倍数
var zoomScale: CGFloat {
  steadyStateZoomScale * gestureZoomScale
}

func zoomGesture() -> some Gesture {
  MagnificationGesture()
    .updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, _ in
      // 缩放过程中,不断地更新 `gestureZoomScale` 的值
      gestureZoomScale = latestGestureScale
    }
    .onEnded { gestureScaleAtEnd in
      // 手势结束,更新 steadyStateZoomScale 的值;
      // 此时 gestureZoomScale 的值会被重置为初始值 1
      steadyStateZoomScale *= gestureScaleAtEnd
      makeSureZoomScaleInBounds()
    }
}

// 确保放大倍数在我们设置的范围内;Haptics 是加上震动效果
func makeSureZoomScaleInBounds() {
  withAnimation {
    if steadyStateZoomScale < 1 {
      steadyStateZoomScale = 1
      Haptics.impact(.light)
    } else if steadyStateZoomScale > maxScale {
      steadyStateZoomScale = maxScale
      Haptics.impact(.light)
    }
  }
}

// Haptics.swift
enum Haptics {
  static func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
    let generator = UIImpactFeedbackGenerator(style: style)
    generator.impactOccurred()
  }
}

到目前为止,我们的图片预览器就实现了。是不是很简单?🤣🤣🤣

但是仔细回顾一下代码,目前这个图片预览器只支持 UIImage 的预览。如果预览器的用户查看的图片是 Image 呢?又或者是其他任何通过 View 来显示的图片呢?所以我们还得进一步增强预览器的可用性。

预览任意 View

既然是任意 View,很容易想到泛型。我们可以将 LBJImagePreviewer 定义为泛型。代码变动如下:


public struct LBJImagePreviewer<Content: View>: View {
  private let uiImage: UIImage?
  private let contentInfo: (content: Content, aspectRatio: CGFloat)?
  private let maxScale: CGFloat
  
  public init(
    uiImage: UIImage,
    maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale
  ) {
    self.uiImage = uiImage
    self.contentInfo = nil
    self.maxScale = maxScale
  }
  
  public init(
    content: Content,
    aspectRatio: CGFloat,
    maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale
  ) {
    self.uiImage = nil
    self.contentInfo = (content, aspectRatio)
    self.maxScale = maxScale
  }
  
  @ViewBuilder
  var imageContent: some View {
    if let uiImage = uiImage {
      Image(uiImage: uiImage)
        .resizable()
        .aspectRatio(contentMode: .fit)
    } else if let content = contentInfo?.content {
      if let image = content as? Image {
        image.resizable()
      } else {
        content
      }
    }
  }
  
  func imageSize(fits geometry: GeometryProxy) -> CGSize {
    if let uiImage = uiImage {
      let hZoom = geometry.size.width / uiImage.size.width
      let vZoom = geometry.size.height / uiImage.size.height
      return uiImage.size * min(hZoom, vZoom)
      
    } else if let contentInfo = contentInfo {
      let geoRatio = geometry.size.width / geometry.size.height
      let imageRatio = contentInfo.aspectRatio
      
      let width: CGFloat
      let height: CGFloat
      if imageRatio < geoRatio {
        height = geometry.size.height
        width = height * imageRatio
      } else {
        width = geometry.size.width
        height = width / imageRatio
      }
      
      return .init(width: width, height: height)
    }
    
    return .zero
  }
}

从代码中可以看到,如果是用 content 来初始化预览器,还需要传入 aspectRatio (宽高比),因为不能从传入的 content 得到它的比例,所以需要外部告诉我们。

通过修改,目前的图片预览器就可以支持任意 View 的缩放了。但如果我们就是要预览 UIImage,在初始化预览器的时候,它还要求指定泛型的具体类型。例如:


// EmptyView 可以换成其他任意遵循 `View` 协议的类型
LBJImagePreviewer<EmptyView>(uiImage: UIImage(named: "IMG_0001")!)

如果不加上 <EmptyView> 就会报错,这显然是不合理的设计。我们还得进一步优化。

将 UIImage 从 LBJImagePreviewer 剥离

在预览 UIImage 时,不需要用到任何与泛型有关的代码,所以只能将 UIImage 从 LBJImagePreviewer 剥离出来。

从复用代码的角度出发,我们可以想到新定义一个 LBJUIImagePreviewer 专门用于预览 UIImage,内部实现直接调用 LBJImagePreviewer 即可。

LBJUIImagePreviewer 的代码如下:


public struct LBJUIImagePreviewer: View {

  private let uiImage: UIImage
  private let maxScale: CGFloat

  public init(
    uiImage: UIImage,
    maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale
  ) {
    self.uiImage = uiImage
    self.maxScale = maxScale
  }

  public var body: some View {
    // LBJImagePreviewer 重命名为 LBJViewZoomer
    LBJViewZoomer(
      content: Image(uiImage: uiImage),
      aspectRatio: uiImage.size.width / uiImage.size.height,
      maxScale: maxScale
    )
  }
}

将 UIImage 从 LBJImagePreviewer 剥离后,LBJImagePreviewer 的职责只负责缩放 View,所以应该给它重命名,我将它改为 LBJViewZoomer。完整代码如下:


public struct LBJViewZoomer<Content: View>: View {

  private let contentInfo: (content: Content, aspectRatio: CGFloat)
  private let maxScale: CGFloat

  public init(
    content: Content,
    aspectRatio: CGFloat,
    maxScale: CGFloat = LBJImagePreviewerConstants.defaultMaxScale
  ) {
    self.contentInfo = (content, aspectRatio)
    self.maxScale = maxScale
  }

  @State
  private var steadyStateZoomScale: CGFloat = 1

  @GestureState
  private var gestureZoomScale: CGFloat = 1

  public var body: some View {
    GeometryReader { geometry in
      let zoomedImageSize = zoomedImageSize(in: geometry)
      ScrollView([.vertical, .horizontal]) {
        imageContent
          .gesture(doubleTapGesture())
          .gesture(zoomGesture())
          .frame(
            width: zoomedImageSize.width,
            height: zoomedImageSize.height
          )
          .padding(.vertical, (max(0, geometry.size.height - zoomedImageSize.height) / 2))
      }
      .background(Color.black)
    }
    .ignoresSafeArea()
  }
}

// MARK: - Subviews
private extension LBJViewZoomer {
  @ViewBuilder
  var imageContent: some View {
    if let image = contentInfo.content as? Image {
      image
        .resizable()
        .aspectRatio(contentMode: .fit)
    } else {
      contentInfo.content
    }
  }
}

// MARK: - Gestures
private extension LBJViewZoomer {

  // MARK: Tap

  func doubleTapGesture() -> some Gesture {
    TapGesture(count: 2)
      .onEnded {
        withAnimation {
          if zoomScale > 1 {
            steadyStateZoomScale = 1
          } else {
            steadyStateZoomScale = maxScale
          }
        }
      }
  }

  // MARK: Zoom

  var zoomScale: CGFloat {
    steadyStateZoomScale * gestureZoomScale
  }

  func zoomGesture() -> some Gesture {
    MagnificationGesture()
      .updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, _ in
        gestureZoomScale = latestGestureScale
      }
      .onEnded { gestureScaleAtEnd in
        steadyStateZoomScale *= gestureScaleAtEnd
        makeSureZoomScaleInBounds()
      }
  }

  func makeSureZoomScaleInBounds() {
    withAnimation {
      if steadyStateZoomScale < 1 {
        steadyStateZoomScale = 1
        Haptics.impact(.light)
      } else if steadyStateZoomScale > maxScale {
        steadyStateZoomScale = maxScale
        Haptics.impact(.light)
      }
    }
  }
}

// MARK: - Helper Methods
private extension LBJViewZoomer {

  func imageSize(fits geometry: GeometryProxy) -> CGSize {
    let geoRatio = geometry.size.width / geometry.size.height
    let imageRatio = contentInfo.aspectRatio

    let width: CGFloat
    let height: CGFloat
    if imageRatio < geoRatio {
      height = geometry.size.height
      width = height * imageRatio
    } else {
      width = geometry.size.width
      height = width / imageRatio
    }

    return .init(width: width, height: height)
  }

  func zoomedImageSize(in geometry: GeometryProxy) -> CGSize {
    imageSize(fits: geometry) * zoomScale
  }
}

另外,为了方便预览 Image 类型的图片,我们可以定义一个类型:


public typealias LBJImagePreviewer = LBJViewZoomer<Image>

至此,我们的图片预览器就真正完成了。我们一共给外部暴露了三个类型:


LBJUIImagePreviewer
LBJImagePreviewer
LBJViewZoomer

源码

我已经将图片预览器制作成一个 Swift Package,大家可以点击查看。LBJImagePreviewer

在源码中,我在 LBJViewZoomer 多添加了一个属性 doubleTapScale,表示双击放大时的倍数,进一步优化用户使用体验。

总结

这个图片预览器的实现难度并不高,关键点在于对 ScrollView 和放大手势的理解。
存在问题

双击放大时,图片只能从中间位置放大,无法在点击位置放大。(目前 ScrollView 无法手动设置 contentOffset,等待 ScrollView 更新以解决这个问题。)

到此这篇关于如何利用SwiftUI实现可缩放图片预览器的文章就介绍到这了,更多相关SwiftUI可缩放图片预览器内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

如何利用SwiftUI实现可缩放的图片预览器

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

vue如何实现图片预览放大以及缩小效果

这篇文章主要介绍了vue如何实现图片预览放大以及缩小效果的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇vue如何实现图片预览放大以及缩小效果文章都会有所收获,下面我们一起来看看吧。vue图片预览放大以及缩小在v
2023-07-05

Android应用中怎么实现一个图片预览缩放功能

Android应用中怎么实现一个图片预览缩放功能?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。具体实现方法如下2023-05-31

如何利用QT实现图片浏览器

这篇文章主要介绍了如何利用QT实现图片浏览器的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇如何利用QT实现图片浏览器文章都会有所收获,下面我们一起来看看吧。1、概述案例:制作一个小的图片浏览器,要求可以显示jp
2023-07-05

如何使用CSS实现图片的缩放效果

如何使用CSS实现图片的缩放效果在网页设计中,图片的缩放效果是常见的需求之一。通过CSS的相关属性和技巧,我们可以轻松地实现图片的缩放效果。下面,将为大家详细介绍如何使用CSS来实现图片的缩放效果,并给出具体的代码示例。使用transfor
如何使用CSS实现图片的缩放效果
2023-11-21

如何在Android中利用imageview实现一个图片缩放功能

如何在Android中利用imageview实现一个图片缩放功能?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。Android 自定义imageview实现图片缩放实例详解 觉得
2023-05-31

AJAX如何实现图片预览与上传及生成缩略图的方法

这篇文章将为大家详细讲解有关AJAX如何实现图片预览与上传及生成缩略图的方法,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。JS代码://ajax保存数据,后台方法里实现此方法 function SaveD
2023-06-08

如何利用canvas实现图片压缩功能

小编给大家分享一下如何利用canvas实现图片压缩功能,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!项目中做身份证识别时,需要传送图片的 base64 格式编码,
2023-06-09

如何用js实现手指缩放图片功能

这篇“如何用js实现手指缩放图片功能”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“如何用js实现手指缩放图片功能”文章吧。需
2023-06-26

JavaScript如何实现购物车图片局部放大预览效果

这篇文章主要介绍了JavaScript如何实现购物车图片局部放大预览效果的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇JavaScript如何实现购物车图片局部放大预览效果文章都会有所收获,下面我们一起来看看吧
2023-07-05

如何通过纯CSS实现图片的缩放放大效果

在现代网页设计中,图片的缩放放大效果常常被使用。通过CSS,我们可以轻松地实现这一效果,而无需使用JavaScript或其他编程语言。本文将介绍如何使用纯CSS来实现图片的缩放放大效果,并提供具体的代码示例。实现图片的缩放放大效果可以使用C
2023-10-21

Vue使用v-viewer插件实现图片预览和缩放和旋转等功能(推荐)

v-viewer是一个基于viewerjs封装的vue图片预览组件,有预览缩放拉伸旋转切换拖拽等功能,支持配置化,这篇文章主要介绍了Vue使用v-viewer插件实现图片预览和缩放和旋转等功能,需要的朋友可以参考下
2023-02-09

Node如何实现在浏览器预览项目的所有图片详解

最近项目遇到了个需求,需要将存放图片进行预览,所以这篇文章主要给大家介绍了关于Node如何实现在浏览器预览项目的所有图片的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
2023-01-07

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录