Skip to content

Instantly share code, notes, and snippets.

@wingovers
Last active July 1, 2024 04:32
Show Gist options
  • Save wingovers/4d58ad9c24e0af2fb1fd243b6f157a02 to your computer and use it in GitHub Desktop.
Save wingovers/4d58ad9c24e0af2fb1fd243b6f157a02 to your computer and use it in GitHub Desktop.
Transparent SwiftUI view clipped, cropped screenshot returned as UIImage (draft)

Had the darnedest time get the origin set correctly.

A few things that weren't obvious (to me at least):

  1. Convert the target child view to a UIHostingView, not self or body, otherwise your origin will be inexplicably quite off and screenshot will look drunk.
  2. Add +1 to the origin's y-axis, at least on the iPhone XS, because of course. Otherwise see above.
  3. A drawing group must be applied within the background modifier of your view, if your view has a background. Otherwise what's below bleeds through. And if you apply that else where that hierarchy disappears.
  4. In UIGraphics, override the background color to clear. UIHostingView conveniently resets the background to the system background color after the graphic returns. If you don't do this, within UIGraphics you can layer things transparently behind the UIView, but the readout back to SwiftUI will not be the cropped droid you were looking for.
  5. Scale and rotate do not work together as ExclusiveGestures. Ok.
  6. All the gestures perform for the user just fine as a ViewModifier, but changes do not show up in the captured image. Ok. Maybe I did something wrong.
struct ScreenshotDemo: View {
@State var croppedOutput: UIImage?
let squareBounds: CGFloat = 250
// MARK: - Panning / Cropping Overlay
@State var reset = false
@State var position: CGSize = .zero
@State var scale: CGFloat = 1
@State var rotation: Angle = .degrees(0)
@GestureState var positioning: CGSize = .zero
@GestureState var scaling: CGFloat = 1
@GestureState var rotating: Angle = .degrees(0)
var input: some View {
let rotateAndScale = SimultaneousGesture(RotationGesture(minimumAngleDelta: .degrees(5)), MagnificationGesture())
.updating($rotating) { (values, state, _) in
guard let value = values.first,
(Double(-90)...Double(90)).contains(state.degrees + rotation.degrees) else { return }
state = value
}
.updating($scaling) { (values, state, _) in
guard let value = values.second else { return }
state = value
}
.onEnded { values in
if let angle = values.first {
rotation += angle
}
if let magnitude = values.second {
scale *= magnitude
}
}
let drag = DragGesture()
.updating($positioning) { (value, state, _) in
state = value.translation
}
.onEnded {
self.position.height += $0.translation.height
self.position.width += $0.translation.width
}
return
GeometryReader { geo in
ZStack {
Image("glacier")
.resizable()
.rotationEffect(rotating + rotation)
.offset(x: position.width + positioning.width,
y: position.height + positioning.height)
.contentShape(Rectangle())
.scaleEffect(min(10, max(1, scale * scaling)))
.gesture(drag)
.gesture(rotateAndScale)
.clipShape(Circle())
.clipped()
.onTapGesture {
let origin = CGPoint(x: geo.frame(in: .local).origin.x,
y: geo.frame(in: .local).origin.y + 1) // Why +1? NO CLUE.
let rect = CGRect(origin: origin, size: geo.size)
croppedOutput = self.input.screenshot(of: rect) ?? UIImage() // Use self.input, not self
}
}
}.frame(width: squareBounds, height: squareBounds)
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
buttons
Spacer()
input
Spacer()
output
}
.background(
Color(.secondarySystemBackground)
.drawingGroup() // Necessary for transparency
)
}
var output: some View {
VStack(alignment: .center) {
if let cropped = croppedOutput {
Image(uiImage: cropped)
.resizable()
.scaledToFit()
.transition(AnyTransition.scale.combined(with: .opacity))
.animation(.easeInOut(duration: 1))
} else {
EmptyView()
}
}
.frame(width: squareBounds, height: squareBounds)
.padding()
.background(Color.blue.opacity(0.5))
}
var buttons: some View {
HStack {
Spacer()
resetButton
Spacer()
}
.padding()
.font(.headline)
}
var resetButton: some View {
Button { withAnimation {
position = .zero
scale = 1
rotation = .degrees(0)
croppedOutput = nil
}} label: { Text("Reset") }
.padding()
}
}
import SwiftUI
extension View {
func screenshot(of rect: CGRect) -> UIImage? {
let window = UIWindow(frame: rect)
let host = UIHostingController(rootView: self)
window.addSubview(host.view)
host.view.frame = window.frame
return host.view.asImage
}
}
extension UIView {
var asImage: UIImage? {
// Override UIHostingController resetting this to system background color
backgroundColor = .clear
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0)
defer { UIGraphicsEndImageContext() }
guard let context = UIGraphicsGetCurrentContext() else { return nil }
layer.render(in: context)
return UIGraphicsGetImageFromCurrentImageContext()
}
}
@X901
Copy link

X901 commented Nov 5, 2022

Thank you very much, I spend days trying to save view with Transparency Background .
Your sloution is the only sloution that is working !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment