千家信息网

怎么利用SwiftUI实现可缩放的图片预览器

发表于:2024-11-19 作者:千家信息网编辑
千家信息网最后更新 2024年11月19日,这篇文章给大家分享的是有关怎么利用SwiftUI实现可缩放的图片预览器的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。实现过程程序的初步构想要做一个程序,首先肯定是给它起个名
千家信息网最后更新 2024年11月19日怎么利用SwiftUI实现可缩放的图片预览器

这篇文章给大家分享的是有关怎么利用SwiftUI实现可缩放的图片预览器的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

    实现过程

    程序的初步构想

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

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

    import SwiftUIpublic 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 呢? 点击这里可以找到答案 >>

    显示 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 的大小。这个倍数就是放大的倍数。因此我们定义一个变量记录倍数,然后通过双击手势改变它,就能把图片放大缩小,有变动的代码如下:

    // 当前的放大倍数@Stateprivate var zoomScale: CGFloat = 1public 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 的值@Stateprivate var steadyStateZoomScale: CGFloat = 1// 放大手势缩放过程中产生的倍数变化@GestureStateprivate 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.swiftenum Haptics {  static func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {    let generator = UIImpactFeedbackGenerator(style: style)    generator.impactOccurred()  }}

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

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

    预览任意 View

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

    public struct LBJImagePreviewer: 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(uiImage: UIImage(named: "IMG_0001")!)

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

    将 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: 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: - Subviewsprivate extension LBJViewZoomer {  @ViewBuilder  var imageContent: some View {    if let image = contentInfo.content as? Image {      image        .resizable()        .aspectRatio(contentMode: .fit)    } else {      contentInfo.content    }  }}// MARK: - Gesturesprivate 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 Methodsprivate 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

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

    LBJUIImagePreviewerLBJImagePreviewerLBJViewZoomer

    感谢各位的阅读!关于"怎么利用SwiftUI实现可缩放的图片预览器"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

    图片 代码 倍数 尺寸 手势 类型 大小 可缩 内容 过程 变动 就是 更多 用户 目的 程序 篇文章 进一 支持 更新 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 word绘制数据库表结构 北京雅邦互联网科技有限公司 漳平定制软件开发公司排名 企业微信怎么要设置服务器 华为笔记本录音服务器怎么设置 妄想山海捏脸数据库狼人 华北电力大学国泰安数据库 2022魔兽世界怀旧服新服务器 传奇服务数据库非法连接 国产服务器芯片 河南曙光服务器维修技术 雅安网络安全审计 用服务器做监控存储 汽车网络显示服务器错误怎么办 无盘设置后服务器 温江区软件开发招聘 计算机网络技术影响 软件开发模块设计例子 如何搭建数据库管理系统论文 本服务器 受到 保护 在线 有it男做软件开发吗 mariadb数据库循环 深圳嗨噢网络技术 服务器多重网络环境不可达 浙江互联网科技有限公司英文 深圳六一网络技术有限公司 sql数据库工作怎么样 游戏网络或服务器发生错误 欧专局数据库收录哪些国家 中国网络安全大赛英文怎么说
    0