How to convert a View (not UIView) to an image? - swift

Similar to this thread: How to convert a UIView to an image.
I would like to convert a SwiftUI View rather than a UIView to an image.

Although SwiftUI does not provide a direct method to convert a view into an image, you still can do it. It is a little bit of a hack, but it works just fine.
In the example below, the code captures the image of two VStacks whenever they are tapped. Their contents are converted into a UIImage (that you can later save to a file if you need). In this case, I am just displaying it below.
Note that the code can be improved, but it provides the basics to get you started. I use GeometryReader to get the coordinates of the VStack to capture, but it could be improved with Preferences to make it more robust. Check the links provided, if you need to learn more about it.
Also, in order to convert an area of the screen to an image, we do need a UIView. The code uses UIApplication.shared.windows[0].rootViewController.view to get the top view, but depending on your scenario you may need to get it from somewhere else.
Good luck!
And this is the code (tested on iPhone Xr simulator, Xcode 11 beta 4):
import SwiftUI
extension UIView {
func asImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
struct ContentView: View {
#State private var rect1: CGRect = .zero
#State private var rect2: CGRect = .zero
#State private var uiimage: UIImage? = nil
var body: some View {
VStack {
HStack {
VStack {
Text("LEFT")
Text("VIEW")
}
.padding(20)
.background(Color.green)
.border(Color.blue, width: 5)
.background(RectGetter(rect: $rect1))
.onTapGesture { self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect1) }
VStack {
Text("RIGHT")
Text("VIEW")
}
.padding(40)
.background(Color.yellow)
.border(Color.green, width: 5)
.background(RectGetter(rect: $rect2))
.onTapGesture { self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect2) }
}
if uiimage != nil {
VStack {
Text("Captured Image")
Image(uiImage: self.uiimage!).padding(20).border(Color.black)
}.padding(20)
}
}
}
}
struct RectGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { proxy in
self.createView(proxy: proxy)
}
}
func createView(proxy: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.rect = proxy.frame(in: .global)
}
return Rectangle().fill(Color.clear)
}
}

Solution
Here is a possible solution that uses a UIHostingController that is inserted in the background of the rootViewController:
func convertViewToData<V>(view: V, size: CGSize, completion: #escaping (Data?) -> Void) where V: View {
guard let rootVC = UIApplication.shared.windows.first?.rootViewController else {
completion(nil)
return
}
let imageVC = UIHostingController(rootView: view.edgesIgnoringSafeArea(.all))
imageVC.view.frame = CGRect(origin: .zero, size: size)
DispatchQueue.main.async {
rootVC.view.insertSubview(imageVC.view, at: 0)
let uiImage = imageVC.view.asImage(size: size)
imageVC.view.removeFromSuperview()
completion(uiImage.pngData())
}
}
You also need a modified version of the asImage extension proposed here by kontiki (setting UIGraphicsImageRendererFormat is necessary as new devices can have 2x or 3x scale):
extension UIView {
func asImage(size: CGSize) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
return UIGraphicsImageRenderer(size: size, format: format).image { context in
layer.render(in: context.cgContext)
}
}
}
Usage
Assuming you have some test view:
var testView: some View {
ZStack {
Color.blue
Circle()
.fill(Color.red)
}
}
you can convert this View to Data which can be used to return an Image (or UIImage):
convertViewToData(view: testView, size: CGSize(width: 300, height: 300)) {
guard let imageData = $0, let uiImage = UIImage(data: imageData) else { return }
return Image(uiImage: uiImage)
}
The Data object can also be saved to file, shared...
Demo
struct ContentView: View {
#State var imageData: Data?
var body: some View {
VStack {
testView
.frame(width: 50, height: 50)
if let imageData = imageData, let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
}
}
.onAppear {
convertViewToData(view: testView, size: .init(width: 300, height: 300)) {
imageData = $0
}
}
}
var testView: some View {
ZStack {
Color.blue
Circle()
.fill(Color.red)
}
}
}

Following kontiki answer, here is the Preferences way
import SwiftUI
struct ContentView: View {
#State private var uiImage: UIImage? = nil
#State private var rect1: CGRect = .zero
#State private var rect2: CGRect = .zero
var body: some View {
VStack {
HStack {
VStack {
Text("LEFT")
Text("VIEW")
}
.padding(20)
.background(Color.green)
.border(Color.blue, width: 5)
.getRect($rect1)
.onTapGesture {
self.uiImage = self.rect1.uiImage
}
VStack {
Text("RIGHT")
Text("VIEW")
}
.padding(40)
.background(Color.yellow)
.border(Color.green, width: 5)
.getRect($rect2)
.onTapGesture {
self.uiImage = self.rect2.uiImage
}
}
if uiImage != nil {
VStack {
Text("Captured Image")
Image(uiImage: self.uiImage!).padding(20).border(Color.black)
}.padding(20)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension CGRect {
var uiImage: UIImage? {
UIApplication.shared.windows
.filter{ $0.isKeyWindow }
.first?.rootViewController?.view
.asImage(rect: self)
}
}
extension View {
func getRect(_ rect: Binding<CGRect>) -> some View {
self.modifier(GetRect(rect: rect))
}
}
struct GetRect: ViewModifier {
#Binding var rect: CGRect
var measureRect: some View {
GeometryReader { proxy in
Rectangle().fill(Color.clear)
.preference(key: RectPreferenceKey.self, value: proxy.frame(in: .global))
}
}
func body(content: Content) -> some View {
content
.background(measureRect)
.onPreferenceChange(RectPreferenceKey.self) { (rect) in
if let rect = rect {
self.rect = rect
}
}
}
}
extension GetRect {
struct RectPreferenceKey: PreferenceKey {
static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) {
value = nextValue()
}
typealias Value = CGRect?
static var defaultValue: CGRect? = nil
}
}
extension UIView {
func asImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}

I came up with a solution when you can save to UIImage a SwiftUI View that is not on the screen.
The solution looks a bit weird, but works fine.
First create a class that serves as connection between UIHostingController and your SwiftUI. In this class, define a function that you can call to copy your "View's" image. After you do this, simply "Publish" new value to update your views.
class Controller:ObservableObject {
#Published var update=false
var img:UIImage?
var hostingController:MySwiftUIViewHostingController?
init() {
}
func copyImage() {
img=hostingController?.copyImage()
update=true
}
}
Then wrap your SwiftUI View that you want to copy via UIHostingController
class MySwiftUIViewHostingController: UIHostingController<TestView> {
override func viewDidLoad() {
super.viewDidLoad()
}
func copyImage()->UIImage {
let renderer = UIGraphicsImageRenderer(bounds: self.view.bounds)
return renderer.image(actions: { (c) in
self.view.layer.render(in: c.cgContext)
})
}
}
The copyImage() function returns the controller's view as UIImage
Now you need to present UIHostingController:
struct MyUIViewController:UIViewControllerRepresentable {
#ObservedObject var cntrl:Controller
func makeUIViewController(context: Context) -> MySwiftUIViewHostingController {
let controller=MySwiftUIViewHostingController(rootView: TestView())
cntrl.hostingController=controller
return controller
}
func updateUIViewController(_ uiViewController: MySwiftUIViewHostingController, context: Context) {
}
}
And the rest as follows:
struct TestView:View {
var body: some View {
VStack {
Text("Title")
Image("img2")
.resizable()
.aspectRatio(contentMode: .fill)
Text("foot note")
}
}
}
import SwiftUI
struct ContentView: View {
#ObservedObject var cntrl=Controller()
var body: some View {
ScrollView {
VStack {
HStack {
Image("img1")
.resizable()
.scaledToFit()
.border(Color.black, width: 2.0)
.onTapGesture(count: 2) {
print("tap registered")
self.cntrl.copyImage()
}
Image("img1")
.resizable()
.scaledToFit()
.border(Color.black, width: 2.0)
}
TextView()
ImageCopy(cntrl: cntrl)
.border(Color.red, width: 2.0)
TextView()
TextView()
TextView()
TextView()
TextView()
MyUIViewController(cntrl: cntrl)
.aspectRatio(contentMode: .fit)
}
}
}
}
struct ImageCopy:View {
#ObservedObject var cntrl:Controller
var body: some View {
VStack {
Image(uiImage: cntrl.img ?? UIImage())
.resizable()
.frame(width: 200, height: 200, alignment: .center)
}
}
}
struct TextView:View {
var body: some View {
VStack {
Text("Bla Bla Bla Bla Bla ")
Text("Bla Bla Bla Bla Bla ")
Text("Bla Bla Bla Bla Bla ")
Text("Bla Bla Bla Bla Bla ")
Text("Bla Bla Bla Bla Bla ")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You need img1 and img2 (the one that gets copied). I put everything into a scrollview so that one can see that the image copies fine even when not on the screen.

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")
}
}
}
}
}

Uploading user image and saving over default image

So I am working on users uploading images to a "profile picture" of sorts. So my image picker I believe is working fine. The issue is it uploads, but the default image still shows initially.
I know this because if I go to upload another image, for a brief moment the default picture disappears and the new uploaded image can be seen. My button triggers the showingImagePicker bool which shows the new image and should hide the default image, I don't understand why the default image would still be showing.
struct RecipeEditorImage: View {
#State private var showingImagePicker = false
#State private var inputImage: UIImage?
#State private var image: Image?
var body: some View {
ZStack (alignment: .trailing){
if showingImagePicker{
image?
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:200, height: 100)
}
else{
Image("ExampleRecipePicture")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:200, height: 100)
}
Image(systemName:("plus.circle.fill")).renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.offset(x: 20)
.foregroundColor(Color("completeGreen"))
.frame(width:50, height:10)
.onTapGesture {
showingImagePicker = true
}
.sheet(isPresented: $showingImagePicker){
EditorImagePicker(image: $inputImage)
}
.onChange(of: inputImage){ _ in loadImage() }
}
}
func loadImage(){
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
}
}
import PhotosUI
import SwiftUI
struct EditorImagePicker: UIViewControllerRepresentable{
#Binding var image: UIImage?
class Coordinator: NSObject, PHPickerViewControllerDelegate{
var parent: EditorImagePicker
init(_ parent: EditorImagePicker){
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let provider = results.first?.itemProvider else { return }
if provider.canLoadObject(ofClass: UIImage.self){
provider.loadObject(ofClass: UIImage.self){image, _ in
self.parent.image = image as? UIImage
}
}
}
}
func makeUIViewController(context: Context) -> PHPickerViewController {
//configures ios to just be able to select images
var config = PHPickerConfiguration()
config.filter = .images
//the view of picker
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
//leave empty for now
}
}
There are a few issues:
Your first if clause guarantees the chosen image will only be shown if the sheet is shown as well.
You probably want to avoid the extra step of storing a reference to the Image View -- just use it dynamically. That gets rid of the onChange as well.
Your loadObject closure needs to call back to the main thread.
import SwiftUI
import PhotosUI
struct RecipeEditorImage: View {
#State private var showingImagePicker = false
#State private var inputImage: UIImage?
var body: some View {
ZStack (alignment: .trailing){
if let inputImage = inputImage {
Image(uiImage: inputImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:200, height: 100)
} else{
Image(systemName: "pencil")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:200, height: 100)
}
Image(systemName:("plus.circle.fill")).renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.offset(x: 20)
.foregroundColor(Color("completeGreen"))
.frame(width:50, height:10)
.contentShape(Rectangle())
.onTapGesture {
showingImagePicker = true
}
.sheet(isPresented: $showingImagePicker){
EditorImagePicker(image: $inputImage)
}
}
}
}
struct EditorImagePicker: UIViewControllerRepresentable{
#Binding var image: UIImage?
class Coordinator: NSObject, PHPickerViewControllerDelegate{
var parent: EditorImagePicker
init(_ parent: EditorImagePicker){
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let provider = results.first?.itemProvider else { return }
if provider.canLoadObject(ofClass: UIImage.self){
provider.loadObject(ofClass: UIImage.self){image, _ in
DispatchQueue.main.async {
self.parent.image = image as? UIImage
}
}
}
}
}
func makeUIViewController(context: Context) -> PHPickerViewController {
//configures ios to just be able to select images
var config = PHPickerConfiguration()
config.filter = .images
//the view of picker
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
//leave empty for now
}
}

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>) {
}
}

SwiftUI - Hiding a ScrollView's indicators makes it stop scrolling

I'm trying to hide the indicators of a ScrollView but when I try doing so, the ScrollView just doesn't scroll anymore. I'm using macOS if that matters.
ScrollView(showsIndicators: false) {
// Everything is in here
}
On request of #SoOverIt
Demo:
Nothing special, just launched some other test example. Xcode 11.2 / macOS 10.15
var body : some View {
VStack {
ScrollView([.vertical], showsIndicators: false) {
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
}
.frame(height: 100)
.border(Color.blue)
}
.border(Color.red)
}
I fixed the issue.
extension View {
func hideIndicators() -> some View {
return PanelScrollView{ self }
}
}
struct PanelScrollView<Content> : View where Content : View {
let content: () -> Content
var body: some View {
PanelScrollViewControllerRepresentable(content: self.content())
}
}
struct PanelScrollViewControllerRepresentable<Content>: NSViewControllerRepresentable where Content: View{
func makeNSViewController(context: Context) -> PanelScrollViewHostingController<Content> {
return PanelScrollViewHostingController(rootView: self.content)
}
func updateNSViewController(_ nsViewController: PanelScrollViewHostingController<Content>, context: Context) {
}
typealias NSViewControllerType = PanelScrollViewHostingController<Content>
let content: Content
}
class PanelScrollViewHostingController<Content>: NSHostingController<Content> where Content : View {
var scrollView: NSScrollView?
override func viewDidAppear() {
self.scrollView = findNSScrollView(view: self.view)
self.scrollView?.scrollerStyle = .overlay
self.scrollView?.hasVerticalScroller = false
self.scrollView?.hasHorizontalScroller = false
super.viewDidAppear()
}
func findNSScrollView(view: NSView?) -> NSScrollView? {
if view?.isKind(of: NSScrollView.self) ?? false {
return (view as? NSScrollView)
}
for v in view?.subviews ?? [] {
if let vc = findNSScrollView(view: v) {
return vc
}
}
return nil
}
}
Preview:
struct MyScrollView_Previews: PreviewProvider {
static var previews: some View {
ScrollView{
VStack{
Text("hello")
Text("hello")
Text("hello")
Text("hello")
Text("hello")
}
}.hideIndicators()
}
}
So... I think that's the only way for now.
You basically just put a View over your ScrollView indicator with the same backgroundColor as your background View
Note: This obviously only works if your background is static with no content at the trailing edge.
Idea
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme: ColorScheme
let yourBackgroundColorLight: Color = .white
let yourBackgroundColorDark: Color = .black
var yourBackgroundColor: Color { colorScheme == .light ? yourBackgroundColorLight : yourBackgroundColorDark }
var body: some View {
ScrollView {
VStack {
ForEach(0..<1000) { i in
Text(String(i)).frame(width: 280).foregroundColor(.green)
}
}
}
.background(yourBackgroundColor) //<-- Same
.overlay(
HStack {
Spacer()
Rectangle()
.frame(width: 10)
.foregroundColor(yourBackgroundColor) //<-- Same
}
)
}
}
Compact version
You could improve this like that, I suppose you have your color dynamically set up inside assets.
Usage:
ScrollView {
...
}
.hideIndicators(with: <#Your Color#>)
Implementation:
extension View {
func hideIndicators(with color: Color) -> some View {
return modifier(HideIndicators(color: color))
}
}
struct HideIndicators: ViewModifier {
let color: Color
func body(content: Content) -> some View {
content
.overlay(
HStack {
Spacer()
Rectangle()
.frame(width: 10)
.foregroundColor(color)
}
)
}
}

How to use a completion handler to put an image in a SwiftUI view

I have tried this, but I didn't know how to use the results in a SwiftUI View:
func getProfilePicture(_ completion: #escaping ((UIImage) -> Void)) {
Alamofire.request(GIDSignIn.sharedInstance()?.currentUser.profile.imageURL(withDimension: 75) ?? "https://httpbin.org/image/png").responseImage { response in
if let image = response.result.value {
completion(image)
}
}
}
If you can help, I would like to put the returned image from the completion handler in this view:
struct ProfileView: View {
let profileInfo = ProfileInfo()
var placeHolderImage = Image(systemName: "person")
var body: some View {
Group {
placeHolderImage
.clipShape(Circle())
.overlay(
Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.padding(10)
}
}
}
I would like this to return a UIImage so I can eventually use it in a SwiftUI view. I have already tried using a method with an #escaping completion handler, but I couldn't figure out how to use it to fix the issue. Thanks!
You can try something let this:
struct ProfileView: View {
#State var placeHolderImage = Image(systemName: "person")
var body: some View {
Group {
placeHolderImage
.clipShape(Circle())
.overlay(
Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.padding(10)
}.onAppear{
getProfilePicture{ image in
self.placeHolderImage = Image(uiImage: image)
}
}
}
}
When ProfileView appears it will call getProfilePicture. The image specified in image in (when calling the function) is what the completion handler passes through (completion(image)). What you can then do is change your placeHolderImage to what you get in getProfilePicture, but before you do that you need to make your uiImage into an Image. Also make sure you add the #State keyword to your variable so once it changes your View is updated.
Hope that helps!
Use a completion handler as below,
func getProfilePicture(_ completion: #escaping ((UIImage) -> Void)) {
Alamofire.request(GIDSignIn.sharedInstance()?.currentUser.profile.imageURL(withDimension: 75) ?? "https://httpbin.org/image/png").responseImage { response in
if let image = response.result.value {
completion(image)
}
}
}
import SwiftUI
#available(macCatalyst 14.0, *)
#available(iOS 14.0, *)
struct MyUIView : View {
// MARK: - Properties -
#StateObject var myStore : MyStore = MyStore()
#State var imgArray : Array<Image> = Array<Image>.init(repeating: Image("no-image"), count: 100)
// MARK: - View -
var body : some View {
ScrollView {
Text("IMAGES")
LazyVStack (alignment: .leading, spacing: 8.0) {
// actionPlans
ForEach((0 ..< myStore.data.count).clamped(to: 0..<2), id: \.self) { i in
HStack {
let url = myStore.data[i]
let img : Image = imgArray[i]
img
.resizable()
.scaledToFit()
.frame(width: 50, height: 50, alignment: .leading)
.padding(EdgeInsets(top: 0, leading: 0.0, bottom: 0, trailing: 16.0))
.onAppear(){
imageFrom(url: url, completion: { image in
imgArray[i] = Image(uiImage: image!)
})
}
}
Divider()
}
}
}.onAppear(perform: {
fetch()
})
}
// MARK: - Media -
private func fetch() {
DispatchQueue.main.async {
myStore.getData()
}
}
func imageFrom(url: String, completion: #escaping (UIImage?) -> Void) {
// image
if ImageLoader.sharedInstance.checkForImage(url: url as NSString?) {
// has image
completion(ImageLoader.sharedInstance.returnImage(url: url as NSString?)!)
} else {
ImageLoader.sharedInstance.getImage(url: url as NSString?) { (image) in
if image != nil {
completion(image)
} else {
completion(UIImage.init(named: "no-image"))
}
}
}
}
}
#available(macCatalyst 14.0, *)
#available(iOS 14.0, *)
struct MyUIView_Previews: PreviewProvider {
static var previews: some View {
MyUIView()
}
}