SwiftUI,Swift 5 中的类似解决方案
我为此苦苦思考了很久很久,但终于找到了一个可行的解决方案。
我很久没有使用 Swift 或 SwiftUI 进行编码了,我绝对欢迎 cmets 改进此代码。在某些地方,我留下了一些调试代码,您可以取消注释。围绕数学思考所涉及的试验和错误比有能力、经过深思熟虑的方法更多!
此代码中的缺点:首先,最好从 ContentView() 打开图像选择器,然后显示我的自定义视图。我不知道该怎么做。 其次,如果 ContentView() 中已经有图像,最好在自定义视图中填充图像。然而,用户可能期望能够获得“原始”图像并移动它并对其进行缩放。这需要的不仅仅是这个答案所需要的。即,您是否要将原始照片保存在某个 url / 应用程序文件夹以及裁剪版本中?或者甚至保存包含原始图片和重新创建裁剪视图所需的 CGRect 的字典? 第三。如果所选照片正好是屏幕的大小(屏幕截图),则似乎存在错误;它可以缩放得太低。
在一个新的 SwiftUI 生命周期应用中,我有以下 SwiftUI 视图:
这就是你会得到的:
我也使用这个关键的解决方案进行裁剪:
最后,我的一些代码访问了系统 UIcolors,所以我在中使用了扩展
内容视图
import SwiftUI
struct ContentView: View {
@State private var isShowingPhotoSelectionSheet = false
@State private var finalImage: UIImage?
@State private var inputImage: UIImage?
var body: some View {
VStack {
if finalImage != nil {
Image(uiImage: finalImage!)
.resizable()
.frame(width: 100, height: 100)
.scaledToFill()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.shadow(radius: 4)
} else {
Image(systemName: "person.crop.circle.fill")
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.aspectRatio(contentMode: .fit)
.foregroundColor(.systemGray2)
}
Button (action: {
self.isShowingPhotoSelectionSheet = true
}, label: {
Text("Change photo")
.foregroundColor(.systemRed)
.font(.footnote)
})
}
.background(Color.systemBackground)
.statusBar(hidden: isShowingPhotoSelectionSheet)
.fullScreenCover(isPresented: $isShowingPhotoSelectionSheet, onDismiss: loadImage) {
ImageMoveAndScaleSheet(croppedImage: $finalImage)
}
}
func loadImage() {
guard let inputImage = inputImage else { return }
finalImage = inputImage
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
点击/点击Change photo 会弹出下一个视图:
ImagemoveAndScaleSheet
这是一个全屏模式,在打开状态栏时会隐藏。
import SwiftUI
struct ImageMoveAndScaleSheet: View {
@Environment(\.presentationMode) var presentationMode
@State private var isShowingImagePicker = false
///The croped image is what will will send back to the parent view.
///It should be the part of the image in the square defined by the
///cutout circle's diamter. See below, the cutout circle has an "inset" value
///which can be changed.
@Binding var croppedImage: UIImage?
///The input image is received from the ImagePicker.
///We will need to calculate and refer to its aspectr ratio in the functions.
@State private var inputImage: UIImage?
@State private var inputW: CGFloat = 750.5556577
@State private var inputH: CGFloat = 1336.5556577
@State private var theAspectRatio: CGFloat = 0.0
///The profileImage is what wee see on this view. When added from the
///ImapgePicker, it will be sized to fit the screen,
///meaning either its width will match the width of the device's screen,
///or its height will match the height of the device screen.
///This is not suitable for landscape mode or for iPads.
@State private var profileImage: Image?
@State private var profileW: CGFloat = 0.0
@State private var profileH: CGFloat = 0.0
///Zoom and Drag ...
@State private var currentAmount: CGFloat = 0
@State private var finalAmount: CGFloat = 1
@State private var currentPosition: CGSize = .zero
@State private var newPosition: CGSize = .zero
///We track of amount the image is moved for use in functions below.
@State private var horizontalOffset: CGFloat = 0.0
@State private var verticalOffset: CGFloat = 0.0
var body: some View {
ZStack {
ZStack {
Color.black.opacity(0.8)
if profileImage != nil {
profileImage?
.resizable()
.scaleEffect(finalAmount + currentAmount)
.scaledToFill()
.aspectRatio(contentMode: .fit)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
} else {
Image(systemName: "person.crop.circle.fill")
.resizable()
.scaleEffect(finalAmount + currentAmount)
.scaledToFill()
.aspectRatio(contentMode: .fit)
.foregroundColor(.systemGray2)
}
}
Rectangle()
.fill(Color.black).opacity(0.55)
.mask(HoleShapeMask().fill(style: FillStyle(eoFill: true)))
VStack {
Text((profileImage != nil) ? "Move and Scale" : "Select a Photo by tapping the icon below")
.foregroundColor(.white)
.padding(.top, 50)
Spacer()
HStack{
ZStack {
HStack {
Button(
action: {presentationMode.wrappedValue.dismiss()},
label: { Text("Cancel") })
Spacer()
Button(
action: {
self.save()
presentationMode.wrappedValue.dismiss()
})
{ Text("Save") }
.opacity((profileImage != nil) ? 1.0 : 0.2)
.disabled((profileImage != nil) ? false: true)
}
.padding(.horizontal)
.foregroundColor(.white)
Image(systemName: "circle.fill")
.font(.custom("system", size: 45))
.opacity(0.9)
.foregroundColor(.white)
Image(systemName: "photo.on.rectangle")
.imageScale(.medium)
.foregroundColor(.black)
.onTapGesture {
isShowingImagePicker = true
}
}
.padding(.bottom, 5)
}
}
.padding()
}
.edgesIgnoringSafeArea(.all)
//MARK: - Gestures
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
// repositionImage()
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
repositionImage()
}
)
.simultaneousGesture(
DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
self.newPosition = self.currentPosition
repositionImage()
}
)
.simultaneousGesture(
TapGesture(count: 2)
.onEnded({
resetImageOriginAndScale()
})
)
.sheet(isPresented: $isShowingImagePicker, onDismiss: loadImage) {
ImagePicker(image: self.$inputImage)
.accentColor(Color.systemRed)
}
}
//MARK: - functions
private func HoleShapeMask() -> Path {
let rect = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
let insetRect = CGRect(x: inset, y: inset, width: UIScreen.main.bounds.width - ( inset * 2 ), height: UIScreen.main.bounds.height - ( inset * 2 ))
var shape = Rectangle().path(in: rect)
shape.addPath(Circle().path(in: insetRect))
return shape
}
///Called when the ImagePicker is dismissed.
///We want to measure the image receoived and determine the aspect ratio.
private func loadImage() {
guard let inputImage = inputImage else { return }
let w = inputImage.size.width
let h = inputImage.size.height
profileImage = Image(uiImage: inputImage)
inputW = w
inputH = h
theAspectRatio = w / h
resetImageOriginAndScale()
}
///The profileImage will size to fit the screen.
///But we need to know the width and height
///to set the related @State variables.
///Douobke-tpping the image will also set it
///as it was sized originally upon loading.
private func resetImageOriginAndScale() {
withAnimation(.easeInOut){
if theAspectRatio >= screenAspect {
profileW = UIScreen.main.bounds.width
profileH = profileW / theAspectRatio
} else {
profileH = UIScreen.main.bounds.height
profileW = profileH * theAspectRatio
}
currentAmount = 0
finalAmount = 1
currentPosition = .zero
newPosition = .zero
}
}
private func repositionImage() {
//Screen width
let w = UIScreen.main.bounds.width
if theAspectRatio > screenAspect {
profileW = UIScreen.main.bounds.width * finalAmount
profileH = profileW / theAspectRatio
} else {
profileH = UIScreen.main.bounds.height * finalAmount
profileW = profileH * theAspectRatio
}
horizontalOffset = (profileW - w ) / 2
verticalOffset = ( profileH - w ) / 2
///Keep the user from zooming too far in. Adjust as required by the individual project.
if finalAmount > 4.0 {
withAnimation{
finalAmount = 4.0
}
}
///The following if statements keep the image filling the circle cutout.
if profileW >= UIScreen.main.bounds.width {
if newPosition.width > horizontalOffset {
withAnimation(.easeInOut) {
newPosition = CGSize(width: horizontalOffset + inset, height: newPosition.height)
currentPosition = CGSize(width: horizontalOffset + inset, height: currentPosition.height)
}
}
if newPosition.width < ( horizontalOffset * -1) {
withAnimation(.easeInOut){
newPosition = CGSize(width: ( horizontalOffset * -1) - inset, height: newPosition.height)
currentPosition = CGSize(width: ( horizontalOffset * -1 - inset), height: currentPosition.height)
}
}
} else {
withAnimation(.easeInOut) {
newPosition = CGSize(width: 0, height: newPosition.height)
currentPosition = CGSize(width: 0, height: newPosition.height)
}
}
if profileH >= UIScreen.main.bounds.width {
if newPosition.height > verticalOffset {
withAnimation(.easeInOut){
newPosition = CGSize(width: newPosition.width, height: verticalOffset + inset)
currentPosition = CGSize(width: newPosition.width, height: verticalOffset + inset)
}
}
if newPosition.height < ( verticalOffset * -1) {
withAnimation(.easeInOut){
newPosition = CGSize(width: newPosition.width, height: ( verticalOffset * -1) - inset)
currentPosition = CGSize(width: newPosition.width, height: ( verticalOffset * -1) - inset)
}
}
} else {
withAnimation (.easeInOut){
newPosition = CGSize(width: newPosition.width, height: 0)
currentPosition = CGSize(width: newPosition.width, height: 0)
}
}
if profileW <= UIScreen.main.bounds.width && theAspectRatio > screenAspect {
resetImageOriginAndScale()
}
if profileH <= UIScreen.main.bounds.height && theAspectRatio < screenAspect {
resetImageOriginAndScale()
}
}
private func save() {
let scale = (inputImage?.size.width)! / profileW
let xPos = ( ( ( profileW - UIScreen.main.bounds.width ) / 2 ) + inset + ( currentPosition.width * -1 ) ) * scale
let yPos = ( ( ( profileH - UIScreen.main.bounds.width ) / 2 ) + inset + ( currentPosition.height * -1 ) ) * scale
let radius = ( UIScreen.main.bounds.width - inset * 2 ) * scale
croppedImage = imageWithImage(image: inputImage!, croppedTo: CGRect(x: xPos, y: yPos, width: radius, height: radius))
///Debug maths
print("Input: w \(inputW) h \(inputH)")
print("Profile: w \(profileW) h \(profileH)")
print("X Origin: \( ( ( profileW - UIScreen.main.bounds.width - inset ) / 2 ) + ( currentPosition.width * -1 ) )")
print("Y Origin: \( ( ( profileH - UIScreen.main.bounds.width - inset) / 2 ) + ( currentPosition.height * -1 ) )")
print("Scale: \(scale)")
print("Profile:\(profileW) + \(profileH)" )
print("Curent Pos: \(currentPosition.debugDescription)")
print("Radius: \(radius)")
print("x:\(xPos), y:\(yPos)")
}
let inset: CGFloat = 15
let screenAspect = UIScreen.main.bounds.width / UIScreen.main.bounds.height
}
除了拖动和缩放手势之外,主要的外观和(可能还有清理!)是功能。
- HoleShapeMask()(不记得该代码在哪里,但我知道我在 SO 上得到它。
- repositionImage() (这里非常头疼)
- save() 使用
ImageManipulation.swift 文件中的函数。
图像选择器
同样,这只是来自 Hacking With Swift。 (谢谢保罗!)https://twitter.com/twostraws/
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) var presentationMode
@Binding var image: UIImage?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiImage = info[.originalImage] as? UIImage {
parent.image = uiImage
}
parent.presentationMode.wrappedValue.dismiss()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
ImageManipulation.swift
其中包含以下代码:
import UIKit
func imageWithImage(image: UIImage, croppedTo rect: CGRect) -> UIImage {
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
let drawRect = CGRect(x: -rect.origin.x, y: -rect.origin.y,
width: image.size.width, height: image.size.height)
context?.clip(to: CGRect(x: 0, y: 0,
width: rect.size.width, height: rect.size.height))
image.draw(in: drawRect)
let subImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return subImage!
}
## Colors.swift ##
A handy extension to access system UIColors in SwiftUI:
import Foundation
import SwiftUI
extension Color {
static var label: Color {
return Color(UIColor.label)
}
static var secondaryLabel: Color {
return Color(UIColor.secondaryLabel)
}
static var tertiaryLabel: Color {
return Color(UIColor.tertiaryLabel)
}
static var quaternaryLabel: Color {
return Color(UIColor.quaternaryLabel)
}
static var systemFill: Color {
return Color(UIColor.systemFill)
}
static var secondarySystemFill: Color {
return Color(UIColor.secondarySystemFill)
}
static var tertiarySystemFill: Color {
return Color(UIColor.tertiarySystemFill)
}
static var quaternarySystemFill: Color {
return Color(UIColor.quaternarySystemFill)
}
static var systemBackground: Color {
return Color(UIColor.systemBackground)
}
static var secondarySystemBackground: Color {
return Color(UIColor.secondarySystemBackground)
}
static var tertiarySystemBackground: Color {
return Color(UIColor.tertiarySystemBackground)
}
static var systemGroupedBackground: Color {
return Color(UIColor.systemGroupedBackground)
}
static var secondarySystemGroupedBackground: Color {
return Color(UIColor.secondarySystemGroupedBackground)
}
static var tertiarySystemGroupedBackground: Color {
return Color(UIColor.tertiarySystemGroupedBackground)
}
static var systemRed: Color {
return Color(UIColor.systemRed)
}
static var systemBlue: Color {
return Color(UIColor.systemBlue)
}
static var systemPink: Color {
return Color(UIColor.systemPink)
}
static var systemTeal: Color {
return Color(UIColor.systemTeal)
}
static var systemGreen: Color {
return Color(UIColor.systemGreen)
}
static var systemIndigo: Color {
return Color(UIColor.systemIndigo)
}
static var systemOrange: Color {
return Color(UIColor.systemOrange)
}
static var systemPurple: Color {
return Color(UIColor.systemPurple)
}
static var systemYellow: Color {
return Color(UIColor.systemYellow)
}
static var systemGray: Color {
return Color(UIColor.systemGray)
}
static var systemGray2: Color {
return Color(UIColor.systemGray2)
}
static var systemGray3: Color {
return Color(UIColor.systemGray3)
}
static var systemGray4: Color {
return Color(UIColor.systemGray4)
}
static var systemGray5: Color {
return Color(UIColor.systemGray5)
}
static var systemGray6: Color {
return Color(UIColor.systemGray6)
}
}