Problem with snapshot of a "some View" property - swift

To give the context, I am working on a course given by Udacity (I am a beginner!). In this course, there is a project about creating a Meme application where the user can select or take a picture and add top and bottom text on the image. Moreover, there is a background behind the image in gray color.
The idea is to share the meme created. To do so, I implemented a snapshot function as an extension for the View class :
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
Here is the implementation for the share button :
AnimatedActionButton(title: "Share", systemImage: "square.and.arrow.up") {
items.removeAll()
let snapshot = meme.snapshot()
items.append(snapshot)
showingSharePage = true
}
The AnimatedActionButton is a simple code written to make button creation simple which I learnt in CS193p from Stanford.
Here is the meme property :
var meme: some View {
ZStack {
Rectangle()
.foregroundColor(.gray)
VStack {
TextField("", text: $topText, onEditingChanged: { editing in
if editing && !topTextEdited {
self.$topText.wrappedValue = ""
topTextEdited = true
}
})
.font(.custom("HelveticaNeue-CondensedBlack", size: 40))
.autocapitalization(UITextAutocapitalizationType.allCharacters)
.multilineTextAlignment(.center)
.frame(width: UIScreen.main.bounds.size.width * 3/4)
Group {
if let uiImage = uiImage {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
}
}
TextField("", text: $bottomText, onEditingChanged: { editing in
if editing && !bottomTextEdited {
self.$bottomText.wrappedValue = ""
bottomTextEdited = true
}
})
.font(.custom("HelveticaNeue-CondensedBlack", size: 40))
.autocapitalization(UITextAutocapitalizationType.allCharacters)
.multilineTextAlignment(.center)
.frame(width: UIScreen.main.bounds.size.width * 3/4)
}
}
}
Here is what I see on my device when simulating the app :
Here is what I receive when sharing the meme through the application :
Why is it doing this ? What am I doing wrong here ?

Related

Cannot save Shape drawing as Image to Photos with ImageRenderer View

I've this View with which I want to let user draw over an image, and then save the image + drawing as Image in Photos.
import SwiftUI
struct DrawView2: View {
var image: UIImage
#State var points: [CGPoint] = []
var body: some View {
Image(uiImage: image)
.resizable()
.scaledToFit()
.gesture(
DragGesture()
.onChanged { value in
self.addNewPoint(value)
}
)
.overlay () {
DrawShape(points: points)
.stroke(lineWidth: 50) // here you put width of lines
.foregroundColor(.green)
.clipped()
}
.clipShape(Rectangle())
.contentShape(Rectangle())
}
private func addNewPoint(_ value: DragGesture.Value) {
// here you can make some calculations based on previous points
points.append(value.location)
}
}
struct DrawShape: Shape {
var points: [CGPoint]
// drawing is happening here
func path(in rect: CGRect) -> Path {
var path = Path()
guard let firstPoint = points.first else { return path }
path.move(to: firstPoint)
for pointIndex in 1..<points.count {
path.addLine(to: points[pointIndex])
}
return path
}
}
struct DrawHelper: View {
var image: UIImage
var body: some View {
GeometryReader { proxy in
VStack {
let drawView = DrawView2(image: image)
.aspectRatio(contentMode: .fit)
.scaledToFit()
.frame(width: proxy.size.width,
height: proxy.size.height / 2, alignment: .center)
drawView
.cornerRadius(UIConstants.standardCornerRadius)
Button {
let renderer = ImageRenderer(content: drawView)
if let image = renderer.uiImage {
UIImageWriteToSavedPhotosAlbum (image, nil, nil, nil)
}
} label: {
Text("Save Mask")
}
}
}
}
}
What's happening is:
I'm able to draw on the image correctly but it saves only original image. no DrawingShape included in the final saved image.
Screenshot from the Simulator:
enter image description here
Saved Image from the Photos:
enter image description here
Instead of ImageRenderer, I used following method to save the view as Image but it
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
//let targetSize = CGSize(width: 512, height: 384)
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
I tried another approach of using Canvas View but found it had the same problem.
struct DrawView3: View {
var image: UIImage
#State var points: [CGPoint] = []
var body: some View {
Canvas { context, size in
context.draw(Image(uiImage: image).resizable(), in: CGRect(origin: .zero, size: size))
context.stroke(
DrawShape(points: points).path(in: CGRect(origin: .zero, size: size)),
with: .color(.green),
lineWidth: 50
)
}
.gesture(
DragGesture()
.onChanged { value in
self.addNewPoint(value)
}
.onEnded { value in
// here you perform what you need at the end
}
)
}
private func addNewPoint(_ value: DragGesture.Value) {
// here you can make some calculations based on previous points
points.append(value.location)
}
}
I realized that I have been snapshotting the original state of the View with ImageRenderer. When the View gets updated with the drawing of the shape, the new state was never passed to ImageRenderer.
Fixed this issue by #Binding the points var instead of defining it as #State inside the view.
struct DrawView2: View {
var image: UIImage
#Binding var points: [CGPoint]
var body: some View {
...
}
}
struct DrawingHelper: View {
...
...
var body: some View {
...
let drawView = DrawView2(image: image, points: $points)
...
}
struct DrawHelper: View {
#State var points: [CGPoint] = []
var image: UIImage
var body: some View {
GeometryReader { proxy in
VStack {
let drawView = DrawView2(image: image, points: $points)
.aspectRatio(contentMode: .fit)
.scaledToFit()
.frame(width: proxy.size.width,
height: proxy.size.height / 2, alignment: .center)
drawView
.cornerRadius(UIConstants.standardCornerRadius)
Button {
let renderer = ImageRenderer(content: drawView)
if let image = renderer.uiImage {
UIImageWriteToSavedPhotosAlbum (image, nil, nil, nil)
}
} label: {
Text("Save")
}
}
}
}
}

Swift UI exporting content of canvas

I have a canvas where the user can draw stuff on it. I want to export whatever the user draw on my canvas and I'm using the following extension to get images out of Views
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
But unfortunatelly this extension works fine for buttons on any simple view. Whenever I use it for my canvas, it gives me a blank image, it appears it doesn't get the content of my canvas.
Is it possible to export my canvas content?
Is it because I'm drawing my canvas with a stroke?
This is how I'm using my canvas:
#State private var currentLine = Line(color: .red)
#State private var drawedLines = [Line]()
#State private var selectedColor: Color = .red
#State private var selectedSize = 1.0
private var colors:[Color] = [.red, .green, .blue, .black, .orange, .yellow, .brown, .pink, .purple, .indigo, .cyan, .mint]
var canvasPallete: some View {
return Canvas { context, size in
for line in drawedLines {
var path = Path()
path.addLines(line.points)
context.stroke(path, with: .color(line.color), lineWidth: line.size)
}
}.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged({ value in
let point = value.location
currentLine.points.append(point)
currentLine.color = selectedColor
currentLine.size = selectedSize
drawedLines.append(currentLine)
})
.onEnded({ value in
currentLine = Line(color: selectedColor)
})
)
}
var body: some View {
VStack {
canvasPallete
.frame(width: 300, height: 300)
Divider()
HStack(alignment: .bottom ) {
VStack{
Button {
print("Will save draw image")
let image = canvasPallete.snapshot()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
} label: {
Text("Save")
}
.padding([.bottom], 25)
Button {
drawedLines.removeAll()
} label: {
Image(systemName: "trash.circle.fill")
.font(.system(size: 40))
.foregroundColor(.red)
}.frame(width: 70, height: 50, alignment: .center)
}
VStack(alignment: .leading) {
Text("Colors:")
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(colors, id:\.self){color in
ColoredCircleButton(color: color, selected: color == selectedColor) {
selectedColor = color
}
}
}
.padding(.all, 5)
}
Text("Size:")
Slider(value: $selectedSize, in: 0.2...8)
.padding([.leading,.trailing], 20)
.tint(selectedColor)
}
.padding([.bottom], 10)
}
}.frame(minWidth: 400, minHeight: 400)
}
}
struct ColoredCircleButton: View {
let color: Color
var selected = false
let onTap: (() -> Void)
var body: some View {
Circle()
.frame(width: 25, height: 25)
.foregroundColor(color)
.overlay(content: {
if selected {
Circle().stroke(.gray, lineWidth: 3)
}
})
.onTapGesture {
onTap()
}
}
}
This works fine!
The issue you are encountering is that in your sample code, canvasPallete didn't get a frame modifier when snapshot() was called on it. So it got rendered at a size that's not what you want, probably (0,0).
If you change the snapshot line it will work:
let image = canvasPallete
.frame(width: 300, height: 300)
.snapshot()
One more thing! In iOS 16, there's new API for rendering a view to an Image. So for iOS 16 and up, you can skip the View extension and write:
let renderer = ImageRenderer(content: canvasPallete)
if let image = renderer.uiImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}

Asking for UIHostingController's view returns nil

I have a share button which should share a meme (image + top text + bottom text). Here is the code concerning this button :
Button(
action: {
items.removeAll()
items.append(createImage(from: UIHostingController(rootView: Meme(image: image, topText: topText, bottomText: bottomText)).view))
showingSharePage = true
}
) {
Image(systemName: "square.and.arrow.up")
.font(.title)
}
As you can see, I append the item to be shared to items. What I share is a UIImage generated with a UIView with this function :
func createImage(from view: UIView) -> UIImage {
UIGraphicsBeginImageContextWithOptions(CGSize(width: view.frame.width, height: view.frame.height), true, 1)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let generatedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return generatedImage!
}
But when I run on the simulator or on the device itself, I get this error : "Fatal error: Unexpectedly found nil while unwrapping an Optional value" in the line "view.layer.render(in: UIGraphicsGetCurrentContext()!)" in createImage function.
Here is my meme struct :
struct Meme: View {
#State var image: Image?
#State var topText: String
#State var bottomText: String
var body: some View {
image!
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: UIScreen.main.bounds.size.width)
.overlay(
TextField("TOP", text: $topText)
.foregroundColor(.white)
.font(Font.custom("HelveticaNeue-CondensedBlack", size: 30))
.frame(width: UIScreen.main.bounds.size.width * 0.75)
.multilineTextAlignment(.center)
.padding(.vertical, 50.0)
.onTapGesture {
topText = ""
},
alignment: .top
)
.overlay(
TextField("BOTTOM", text: $bottomText)
.foregroundColor(.white)
.font(Font.custom("HelveticaNeue-CondensedBlack", size: 30))
.frame(width: UIScreen.main.bounds.size.width * 0.75)
.multilineTextAlignment(.center)
.padding(.vertical, 50.0)
.onTapGesture {
bottomText = ""
},
alignment: .bottom
)
}
}
Do you know why it is returning nil ?
Interestingly enough The problem is in these lines
UIHostingController(rootView: Meme(image: image, topText: topText, bottomText: bottomText)).view)
and
UIGraphicsBeginImageContextWithOptions(CGSize(width: view.frame.width, height: view.frame.height), true, 1)
Much to my surprise the view returned by UIHostingViewController has an initial frame of CGRect.zero
So you're actually telling the context to have a point size of zero which apparently it just silently fails for. You can double check this by printing the size in your createImage: function
print(view.frame)
or
print(view.frame.size)
I'm pretty sure that's the problem though.

Add shadow above SwiftUI's TabView

I'm trying to implement TabView in SwiftUI that has the same color as screen background but also has a shadow above it like in this picture:
So far I've been able to properly display color, but I don't know how to add the shadow. This is my code:
struct ContentView: View {
init() {
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
UITabBar.appearance().standardAppearance = appearance
}
var body: some View {
TabView {
Text("First View")
.tabItem {
Image(systemName: "square.and.arrow.down")
Text("First")
}
Text("Second View")
.tabItem {
Image(systemName: "square.and.arrow.up")
Text("Second")
}
}
}
}
Does anyone know how to do this? I would appreciate your help :)
Short answer
I found a solution. You can create your own shadow image and add it to the UITabBar appearance like this:
// load your custom shadow image
let shadowImage: UIImage = ...
//you also need to set backgroundImage, without it shadowImage is ignored
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().shadowImage = shadowImage
More detailed answer
Setting backgroundImage
Note that by setting
UITabBar.appearance().backgroundImage = UIImage()
you make your TabView transparent, so it is not ideal if you have content that can scroll below it. To overcome this, you can set TabView's color.
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor.systemGray6
UITabBar.appearance().standardAppearance = appearance
Setting shadowImage
I wanted to generate shadow image programatically. For that I've created an extension of UIImage. (code taken from here)
extension UIImage {
static func gradientImageWithBounds(bounds: CGRect, colors: [CGColor]) -> UIImage {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
And finally I styled my TabView like this:
let image = UIImage.gradientImageWithBounds(
bounds: CGRect( x: 0, y: 0, width: UIScreen.main.scale, height: 8),
colors: [
UIColor.clear.cgColor,
UIColor.black.withAlphaComponent(0.1).cgColor
]
)
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor.systemGray6
appearance.backgroundImage = UIImage()
appearance.shadowImage = image
UITabBar.appearance().standardAppearance = appearance
Result
Your best bet in order to achieve pretty much exactly what you wish is to create a custom TabView.
In fact, in SwiftUI you could use UITabBarAppearance().shadowColor, but that won't do much apart from drawing a 2px line on top of the TabView itself.
Instead, with the below code, you could create a custom TabView and achieve the desired graphical effect.
import SwiftUI
enum Tab {
case borrow,ret,device,you
}
struct TabView: View {
#Binding var tabIdx: Tab
var body: some View {
HStack {
Group {
Spacer()
Button (action: {
self.tabIdx = .borrow
}) {
VStack{
Image(systemName: "arrow.down.circle")
Text("Borrow")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .borrow ? .purple : .secondary)
Spacer()
Button (action: {
self.tabIdx = .ret
}) {
VStack{
Image(systemName: "arrow.up.circle")
Text("Return")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .ret ? .purple : .secondary)
Spacer()
Button (action: {
self.tabIdx = .device
}) {
VStack{
Image(systemName: "safari")
Text("Device")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .device ? .purple : .secondary)
Spacer()
Button (action: {
self.tabIdx = .you
}) {
VStack{
Image(systemName: "person.circle")
Text("You")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .you ? .purple : .secondary)
Spacer()
}
}
.padding(.bottom, 30)
.padding(.top, 10)
.background(Color(red: 0.95, green: 0.95, blue: 0.95))
.font(.system(size: 30))
.frame(height: 80)
}
}
struct ContentView: View {
#State var tabIdx: Tab = .borrow
var body: some View {
NavigationView {
VStack(spacing: 20) {
Spacer()
if tabIdx == .borrow {
Text("Borrow")
} else if tabIdx == .ret {
Text("Return")
} else if tabIdx == .device {
Text("Device")
} else if tabIdx == .you {
Text("You")
}
Spacer(minLength: 0)
TabView(tabIdx: self.$tabIdx)
.shadow(radius: 10)
}
.ignoresSafeArea()
}
}
}
Remember that when you do this, all your tabs are specified as cases within enum Tab {}, and the TabView() contains some Button elements, which will change the tab by using the #State var tabIdx. So you can adjust your actions by modifying the self.tabIdx = <yourtab> statement.
The active color is set with this statement after each button:
.foregroundColor(self.tabIdx == .borrow ? .purple : .secondary)
Here you can just change .purple to whatever suits you.
You can see that on the ContentView, the if-else if block catches the SubView. Here I have placed some Text() elements like Text("Borrow"), so you can replace them by calling something like BorrowView() or whatever you have.
I have added the shadow when I call the TabView from within the ContentView, but you could even add the .shadow(radius: 10) after the HStack of the TabView itself.
This would be the final output:
                 

Taking screenshot doesn't work when using #State in SwiftUI

I am trying to take a screenshot of the selected area using CGRect. It works fine if I don't use #State variable. But I need to use #State variable too.
Here is my code...
struct ScreenShotTest: View {
#State var abc = 0 //Works well if I remove the line
var body: some View {
Button(action: {
let image = self.takeScreenshot(theRect: CGRect(x: 0, y: 0, width: 200, height: 100))
print(image)
}) {
Text("Take Screenshot")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
}
}
}
extension UIView {
var renderedImage: UIImage {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.main.scale)
let context: CGContext = UIGraphicsGetCurrentContext()!
self.layer.render(in: context)
let capturedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return capturedImage
}
}
extension View {
func takeScreenshot(theRect: CGRect) -> UIImage {
let window = UIWindow(frame: theRect)
let hosting = UIHostingController(rootView: self)
hosting.view.frame = window.frame
window.addSubview(hosting.view)
window.makeKeyAndVisible()
return hosting.view.renderedImage
}
}
I ran into this problem too, but with an EnvironmentObject. I solved it by declaring the EnvironmentObject in an outer view and passing it as a plain variable to the inner view that was crashing when I used it. This is pared way down from my real code, but it should convey the idea.
struct Sample: View {
#State private var isSharePresented: Bool = false
#EnvironmentObject var model: Model
static var screenshot: UIImage?
var body: some View {
GeometryReader { geometry in
// Hack to get a black background.
ZStack {
Spacer()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.black)
.edgesIgnoringSafeArea(.all)
VStack {
CompletionContent(model: Model.theModel)
Spacer()
Button(action: {
model.advanceToNext()
}) {
Text("Continue", comment: "Dismiss this view")
.font(.headline)
.foregroundColor(.init(UIColor.link))
}
HStack {
Spacer()
Button(action: {
Sample.screenshot = self.takeScreenshot(theRect: (geometry.frame(in: .global)))
self.isSharePresented = true
}) {
VStack {
Image("blank")
Image(systemName: "square.and.arrow.up")
.resizable()
.frame(width: 20, height: 30)
}
}
}
Spacer()
}
}
}
.sheet(isPresented: $isSharePresented, onDismiss: {
print("Dismiss")
}, content: {
ActivityViewController(screenshot: Sample.screenshot!)
})
}
}
struct SampleContent : View {
var model: Model
var body: some View {
Text("Content that uses the model variable goes here")
}
}
extension UIView {
var renderedImage: UIImage {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.main.scale)
let context: CGContext = UIGraphicsGetCurrentContext()!
self.layer.render(in: context)
let capturedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return capturedImage
}
}
extension View {
func takeScreenshot(theRect: CGRect) -> UIImage {
let window = UIWindow(frame: theRect)
let hosting = UIHostingController(rootView: self)
hosting.view.frame = window.frame
window.addSubview(hosting.view)
window.makeKeyAndVisible()
return hosting.view.renderedImage
}
}
struct ActivityViewController: UIViewControllerRepresentable {
var screenshot: UIImage
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(
context: UIViewControllerRepresentableContext<ActivityViewController>)
-> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: [screenshot], applicationActivities: applicationActivities)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {
}
}