欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

如何利用SwiftUI實現(xiàn)可縮放的圖片預覽器

 更新時間:2021年09月13日 12:56:21   作者:Lebron  
這篇文章主要給大家介紹了關于如何利用SwiftUI實現(xiàn)可縮放圖片預覽器的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用SwiftUI具有一定的參考學習價值,需要的朋友可以參考下

前言

在開發(fā)中,我們經(jīng)常會遇到點擊圖片查看大圖的需求。在 Apple 的推動下,iOS 開發(fā)必定會從 UIKit 慢慢向 SwiftUI 轉(zhuǎn)變。為了更好地適應這一趨勢,今天我們用 SwiftUI 實現(xiàn)一個可縮放的圖片預覽器。

實現(xiàn)過程

程序的初步構想

要做一個程序,首先肯定是給它起個名字。既然是圖片預覽器(Image Previewer),再加上我自己習慣用的前綴 LBJ,就把它命名為 LBJImagePreviewer 吧。

既然是圖片預覽器,所以需要外部提供圖片給我們;然后是可縮放,所以需要一個最大的縮放倍數(shù)。有了這些思考,可以把 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,這樣做的目的是把代碼中用到的數(shù)值和經(jīng)驗值等整理到一個地方,方便后續(xù)的修改。這是一個好的編程習慣。

細心的讀者可能還會注意到 LBJImagePreviewerConstants 是一個 enum 類型。為什么不用 struct 或者 class 呢?

在 Swift 中定義靜態(tài)方法,class / struct / enum 三者如何選擇?

在開發(fā)過程中,我們經(jīng)常會遇到需要定義一些靜態(tài)方法的需求。通常我們會想到用 class 和 struct 去定義,然而卻忽略了 enum 也可以擁有靜態(tài)方法。那么問題來了:既然三者都可以定義靜態(tài)方法,那么我們應該如何選擇?
下面直接給出答案:

  • class:class 是引用類型,支持繼承。如果你需要這兩個特性,那么選擇 class。
  • struct:struct 是值類型,不支持繼承。如果你需要值類型,并且某些時候需要這個類型的實例,那么用 struct。
  • enum:enum 也是值類型,一般用來定義一組相關的值。如果我們想要的靜態(tài)方法是一系列的工具,不需要任何的實例化和繼承,那么用 enum 最合適。

另外,其實這個規(guī)則也適用于靜態(tài)變量。

顯示 UIImage

當用戶點開圖片預覽器,當然是希望圖片等比例占據(jù)整個圖片預覽器,所以需要知道圖片預覽器當前的尺寸和圖片尺寸,從而通過計算讓圖片等比例占據(jù)整個圖片預覽器。

圖片預覽器當前的尺寸可以通過 GeometryReader 得到;圖片大小可以直接從 UIImage 得到。所以我們可以把

LBJImagePreviewer 的 body 定義如下:

public struct LBJImagePreviewer: View {
  public var body: some View {
    GeometryReader { geometry in                  // 用于獲取圖片預覽器所占據(jù)的尺寸
      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 的內(nèi)容可以滾動起來,必須要讓它的尺寸大于 ScrollView 的尺寸。沿著這個思路可以想到,我們可修改 imageContent 的大小來實現(xiàn)放大縮小,也就是修改下面這個 frame:

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

我們通過用 imageSize(fits: geometry) 的返回值乘以一個倍數(shù),就可以改變 frame 的大小。這個倍數(shù)就是放大的倍數(shù)。因此我們定義一個變量記錄倍數(shù),然后通過雙擊手勢改變它,就能把圖片放大縮小,有變動的代碼如下:

// 當前的放大倍數(shù)
@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。代碼變動如下:

// 穩(wěn)定的放大倍數(shù),放大手勢以此為基準來改變 zoomScale 的值
@State
private var steadyStateZoomScale: CGFloat = 1

// 放大手勢縮放過程中產(chǎn)生的倍數(shù)變化
@GestureState
private var gestureZoomScale: CGFloat = 1

// 變成了只讀屬性,當前圖片被放大的倍數(shù)
var zoomScale: CGFloat {
  steadyStateZoomScale * gestureZoomScale
}

func zoomGesture() -> some Gesture {
  MagnificationGesture()
    .updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, _ in
      // 縮放過程中,不斷地更新 `gestureZoomScale` 的值
      gestureZoomScale = latestGestureScale
    }
    .onEnded { gestureScaleAtEnd in
      // 手勢結(jié)束,更新 steadyStateZoomScale 的值;
      // 此時 gestureZoomScale 的值會被重置為初始值 1
      steadyStateZoomScale *= gestureScaleAtEnd
      makeSureZoomScaleInBounds()
    }
}

// 確保放大倍數(shù)在我們設置的范圍內(nèi);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()
  }
}

到目前為止,我們的圖片預覽器就實現(xiàn)了。是不是很簡單?🤣🤣🤣

但是仔細回顧一下代碼,目前這個圖片預覽器只支持 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` 協(xié)議的類型
LBJImagePreviewer<EmptyView>(uiImage: UIImage(named: "IMG_0001")!)

如果不加上 <EmptyView> 就會報錯,這顯然是不合理的設計。我們還得進一步優(yōu)化。

將 UIImage 從 LBJImagePreviewer 剝離

在預覽 UIImage 時,不需要用到任何與泛型有關的代碼,所以只能將 UIImage 從 LBJImagePreviewer 剝離出來。

從復用代碼的角度出發(fā),我們可以想到新定義一個 LBJUIImagePreviewer 專門用于預覽 UIImage,內(nèi)部實現(xiàn)直接調(diào)用 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

源碼

我已經(jīng)將圖片預覽器制作成一個 Swift Package,大家可以點擊查看。LBJImagePreviewer

在源碼中,我在 LBJViewZoomer 多添加了一個屬性 doubleTapScale,表示雙擊放大時的倍數(shù),進一步優(yōu)化用戶使用體驗。

總結(jié)

這個圖片預覽器的實現(xiàn)難度并不高,關鍵點在于對 ScrollView 和放大手勢的理解。
存在問題

雙擊放大時,圖片只能從中間位置放大,無法在點擊位置放大。(目前 ScrollView 無法手動設置 contentOffset,等待 ScrollView 更新以解決這個問題。)

到此這篇關于如何利用SwiftUI實現(xiàn)可縮放圖片預覽器的文章就介紹到這了,更多相關SwiftUI可縮放圖片預覽器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Swift在控件中添加點擊手勢的方法

    Swift在控件中添加點擊手勢的方法

    這篇文章主要介紹了Swift在控件中添加點擊手勢的方法,本文講解如何在tableview的headerview中添加點擊手勢的方法,需要的朋友可以參考下
    2015-01-01
  • Swift中如何避免循環(huán)引用的方法

    Swift中如何避免循環(huán)引用的方法

    本篇文章主要介紹了Swift中如何避免循環(huán)引用的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-12-12
  • SwiftUI使用Paths和AnimatableData實現(xiàn)酷炫的顏色切換動畫

    SwiftUI使用Paths和AnimatableData實現(xiàn)酷炫的顏色切換動畫

    這篇文章主要介紹了SwiftUI使用Paths和AnimatableData實現(xiàn)酷炫的顏色切換動畫,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2020-05-05
  • Swift data范圍截取問題解決方案

    Swift data范圍截取問題解決方案

    這篇文章主要介紹了Swift data范圍截取問題解決方案,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-09-09
  • 詳解Swift中的下標訪問用法

    詳解Swift中的下標訪問用法

    在Swift中我們可以用subscript函數(shù)來定義下標,從而通過下標來訪問數(shù)組與字典等數(shù)據(jù)結(jié)構,這里我們就來詳解Swift中的下標訪問用法:
    2016-07-07
  • 超全面的Swift編碼規(guī)范(推薦)

    超全面的Swift編碼規(guī)范(推薦)

    這篇文章主要給大家介紹了關于Swift編碼規(guī)范的相關資料,文中介紹的非常詳細,對大家開發(fā)swift具有一定的參考價值,需要的朋友可以參考學習,下面來一起看看吧。
    2017-03-03
  • Swift縮放并填充圖片功能的實現(xiàn)

    Swift縮放并填充圖片功能的實現(xiàn)

    最近有一個需求,就是將圖片先等比例縮放到指定大小,然后將空余出來空間填充為黑色,返回指定大小的圖片。本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2021-11-11
  • swift中的@UIApplicationMain示例詳解

    swift中的@UIApplicationMain示例詳解

    這篇文章主要給大家介紹了關于swift中@UIApplicationMain的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
    2017-12-12
  • 蘋果公司編程語言Swift語言簡介

    蘋果公司編程語言Swift語言簡介

    這篇文章主要介紹了蘋果公司編程語言Swift語言簡介,Swift 是一門新的編程語言,兼容Objective-C代碼,是用來代替Objective-C的蘋果主力開發(fā)語言,需要的朋友可以參考下
    2014-07-07
  • Swift解決UITableView空數(shù)據(jù)視圖問題的簡單方法

    Swift解決UITableView空數(shù)據(jù)視圖問題的簡單方法

    這篇文章主要給大家介紹了關于Swift解決UITableView空數(shù)據(jù)視圖問題的簡單方法,文中通過示例代碼介紹的非常詳細,對大家學習或者使用swift具有一定的參考學習價值,需要的朋友可以參考下
    2018-10-10

最新評論