我正在开发一个 SwiftUI 修改器 (
ZoomableModifier
),它将主要用于使图像可缩放。我希望它可以在 iOS 和 watchOS 上运行,特别是在 watchOS 上。我希望能够使用数字表冠进行放大和缩小,为了实现这一点,我使用 .digitalCrownRotation()
修改器来读取数字表冠的旋转并使用它来确定比例因子。
这是修改器的代码:
//
// ZoomableModifier.swift
//
// Created by jasu on 2022/01/26.
// Modified by Toni Sucic on 2023/01/08.
// Copyright (c) 2022 jasu All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is furnished
// to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import SwiftUI
public struct ZoomableModifier: ViewModifier {
private enum ZoomState {
case inactive
case active(scale: CGFloat)
var scale: CGFloat {
switch self {
case .active(let scale):
return scale
default:
return 1.0
}
}
}
/// The content size of the views.
let contentSize: CGSize
/// The minimum value that can be zoom out.
let min: CGFloat
/// The maximum value that can be zoom in.
let max: CGFloat
/// A value that indicates whether the scroll view displays the scrollable component of the content offset, in a way that’s suitable for the platform.
let showsIndicators: Bool
#if os(iOS)
@GestureState private var zoomState = ZoomState.inactive
#endif
@State private var currentScale: CGFloat = 1.0
var scale: CGFloat {
#if os(iOS)
currentScale * zoomState.scale
#elseif os(watchOS)
currentScale
#endif
}
#if os(iOS)
var zoomGesture: some Gesture {
MagnificationGesture()
.updating($zoomState) { value, state, transaction in
state = .active(scale: value)
}
.onEnded { value in
var new = currentScale * value
if new <= min { new = min }
if new >= max { new = max }
currentScale = new
}
}
#endif
var doubleTapGesture: some Gesture {
TapGesture(count: 2).onEnded {
if scale <= min { currentScale = max } else
if scale >= max { currentScale = min } else {
currentScale = ((max - min) * 0.5 + min) < scale ? max : min
}
}
}
public func body(content: Content) -> some View {
ScrollView([.vertical, .horizontal], showsIndicators: showsIndicators) {
content
.frame(width: contentSize.width * scale, height: contentSize.height * scale, alignment: .center)
.scaleEffect(scale, anchor: .center)
#if os(watchOS)
.focusable(true)
.digitalCrownRotation(detent: $currentScale,
from: min,
through: max,
by: 0.1,
sensitivity: .low,
isContinuous: false,
isHapticFeedbackEnabled: true)
#endif
}
#if os(iOS)
.gesture(ExclusiveGesture(zoomGesture, doubleTapGesture))
#elseif os(watchOS)
.gesture(doubleTapGesture)
#endif
.animation(.easeInOut, value: scale)
}
}
extension View {
func zoomable(contentSize: CGSize,
min: CGFloat = 1.0,
max: CGFloat = 3.0,
showsIndicators: Bool = false) -> some View {
modifier(ZoomableModifier(contentSize: contentSize, min: min, max: max, showsIndicators: showsIndicators))
}
}
我在此视图中使用修改器:
struct FullscreenImage: View {
let imageURL: URL
@Environment(\.dismiss) var dismiss
var body: some View {
GeometryReader { proxy in
AsyncImage(url: imageURL) { imagePhase in
Group {
switch imagePhase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: proxy.size.width, height: proxy.size.height)
.zoomable(contentSize: proxy.size)
case .failure(let error):
Text(error.localizedDescription)
.font(.caption)
.padding()
default:
Rectangle()
.fill(Color.white.opacity(0.2))
}
}
.animation(.easeInOut, value: imagePhase)
}
}
}
}
我希望有一种方法可以在使用
.digitalCrownRotation()
修饰符时禁用滚动视图的数字表冠滚动,但我还没有找到。因此,当我旋转数码表冠时,它会尝试滚动滚动视图,而不是更改比例因子来获得我正在寻找的缩放效果。
我通过用 VStack 替换 ScrollView 来实现您想要的功能(在不同的上下文中),然后将数字表冠读数附加到我想要缩放的视图的比例,然后将 .gesture 修改器附加到视图并将其提供给.offset 修饰符的 x 和 y 值,附加到嵌入视图(如果您需要滚动效果)。