I have a very simple app: 4,000 images(400x400 in .jpg) that need to display 3 in a row. All images a stored in file storage and are equal 1/3 of device width so I don't need to resize/rescale it but display as is.
Here is a body code:
var body: some View {
ScrollView {
LazyVGrid(columns: Array(repeating: GridItem(.fixed(size), spacing: 1), count: 3),
alignment: .center,
spacing: 0) {
ForEach(controller.newAlbums, id: \.self) { album in
PhotoThumbnailView(filePath: album)
}
}
}
.task {
controller.loadUserAlbums()
}
}
let size = UIScreen.main.bounds.width/3
struct PhotoThumbnailView: View {
#State private var image: Image?
#State private var imageFileURL: String
init(filePath: String) {
self.imageFileURL = filePath
}
func readFromFile(){
if let uiImage = UIImage(contentsOfFile: imageFileURL) {
self.image = Image(uiImage: uiImage)
}
}
var body: some View {
ZStack {
if let img = image {
img
} else {
Rectangle()
.foregroundColor(.clear)
.frame(width: size, height: size)
ProgressView()
}
}
.task({
readFromFile()
})
.onDisappear {
image = nil
}
}
}
The problem is that when I scroll down (even vary slowly) it lagging and the scrolling animation is not smooth. How to resolve the issue above?
Related
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")
}
}
}
}
}
I'm creating vertical layout which has scrollable horizontal LazyHGrid in it. The problem is that views in LazyHGrid can have different heights (primarly because of dynamic text lines) but the grid always calculates height of itself based on first element in grid:
What I want is changing size of that light red rectangle based on visible items, so when there are smaller items visible it should look like this:
and when there are bigger items it should look like this:
This is code which results in state on the first image:
struct TestView: PreviewProvider {
static var previews: some View {
ScrollView {
VStack {
Color.blue
.frame(height: 100)
ScrollView(.horizontal) {
LazyHGrid(
rows: [GridItem()],
alignment: .top,
spacing: 16
) {
Color.red
.frame(width: 64, height: 24)
ForEach(Array(0...10), id: \.self) { value in
Color.red
.frame(width: 64, height: CGFloat.random(in: 32...92))
}
}.padding()
}.background(Color.red.opacity(0.3))
Color.green
.frame(height: 100)
}
}
}
}
Something similar what I want can be achieved by this:
extension View {
func readSize(edgesIgnoringSafeArea: Edge.Set = [], onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
SwiftUI.Color.clear
.preference(key: ReadSizePreferenceKey.self, value: geometryProxy.size)
}.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
)
.onPreferenceChange(ReadSizePreferenceKey.self) { size in
DispatchQueue.main.async { onChange(size) }
}
}
}
struct ReadSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
struct Size: Equatable {
var height: CGFloat
var isValid: Bool
}
struct TestView: View {
#State private var sizes = [Int: Size]()
#State private var height: CGFloat = 32
static let values: [(Int, CGFloat)] =
(0...3).map { ($0, CGFloat(32)) }
+ (4...10).map { ($0, CGFloat(92)) }
var body: some View {
ScrollView {
VStack {
Color.blue
.frame(height: 100)
ScrollView(.horizontal) {
LazyHGrid(
rows: [GridItem(.fixed(height))],
alignment: .top,
spacing: 16
) {
ForEach(Array(Self.values), id: \.0) { value in
Color.red
.frame(width: 300, height: value.1)
.readSize { sizes[value.0]?.height = $0.height }
.onAppear {
if sizes[value.0] == nil {
sizes[value.0] = Size(height: .zero, isValid: true)
} else {
sizes[value.0]?.isValid = true
}
}
.onDisappear { sizes[value.0]?.isValid = false }
}
}.padding()
}.background(Color.red.opacity(0.3))
Color.green
.frame(height: 100)
}
}.onChange(of: sizes) { sizes in
height = sizes.filter { $0.1.isValid }.map { $0.1.height }.max() ?? 32
}
}
}
... but as you see its kind of laggy and a little bit complicated, isn't there better solution? Thank you everyone!
The height of a row in a LazyHGrid is driven by the height of the tallest cell. According to the example you provided, the data source will only show a smaller height if it has only a small size at the beginning.
Unless the first rendering will know that there are different heights, use the larger value as the height.
Is your expected UI behaviour that the height will automatically switch? Or use the highest height from the start.
Here is my View:
I want all boxes (the red rectangles) to be the same size (heights all equal to each other and widths all equal to each other). They don't need to be square.
When I create views using Image(systemname:) they have different intrinsic sizes. How do I make them the same size without hard-coding the size.
struct InconsistentSymbolSizes: View {
let symbols = [ "camera", "comb", "diamond", "checkmark.square"]
var body: some View {
HStack(spacing: 0) {
ForEach(Array(symbols), id: \.self) { item in
VStack {
Image(systemName: item).font(.largeTitle)
}
.padding()
.background(.white)
.border(.red)
}
}
.border(Color.black)
}
}
If you want to normalize the sizes, you could use a PreferenceKey to measure the largest size and make sure that all of the other sizes expand to that:
struct ContentView: View {
let symbols = [ "camera", "comb", "diamond", "checkmark.square"]
#State private var itemSize = CGSize.zero
var body: some View {
HStack(spacing: 0) {
ForEach(Array(symbols), id: \.self) { item in
VStack {
Image(systemName: item).font(.largeTitle)
}
.padding()
.background(GeometryReader {
Color.clear.preference(key: ItemSize.self,
value: $0.frame(in: .local).size)
})
.frame(width: itemSize.width, height: itemSize.height)
.border(.red)
}.onPreferenceChange(ItemSize.self) {
itemSize = $0
}
}
.border(Color.black)
}
}
struct ItemSize: PreferenceKey {
static var defaultValue: CGSize { .zero }
static func reduce(value: inout Value, nextValue: () -> Value) {
let next = nextValue()
value = CGSize(width: max(value.width,next.width),
height: max(value.height,next.height))
}
}
I'm making an iOS app and I want to display some sort of default image (or just blank) in a row view, and then update them to new selected images by users. How the app currently looks like
How can I update the images in the cells to be the new ones users select?
This is how I am creating the row views and cells
import SwiftUI
struct Row: Identifiable {
let id = UUID()
let cells: [Cell]
}
extension Row {
}
extension Row {
static func all() -> [Row] {
var resultRow = [Row]()
for _ in 1...10 {
resultRow.append(Row(cells: [Cell(imageURL: "camera"), Cell(imageURL: "camera")]))
}
return resultRow
}
}
struct Cell: Identifiable {
let id = UUID()
let imageURL: String
}
And my UIView looks like this
import SwiftUI
struct PhotoUIView: View {
#State private var showImagePicker: Bool = false
#State private var image: Image? = nil
let rows = Row.all()
var body: some View {
VStack{
List {
ForEach(rows) { row in
HStack(alignment: .center) {
ForEach(row.cells) { cell in
Image(cell.imageURL)
.resizable()
.scaledToFit()
}
}
}
}.padding(EdgeInsets.init(top: 0, leading: -20, bottom: 0, trailing: -20))
image?.resizable()
.scaledToFit()
if (image == nil) {
Text("Upload photo to begin quick design")
}
Button("Select photo to upload") {
self.showImagePicker = true
}.padding()
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(10)
Button("Begin Quick Design") {
print("Here we upload photo and get result back")
}.padding()
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
}.sheet(isPresented: self.$showImagePicker) {
PhotoCaptureView(showImagePicker: self.$showImagePicker, image: self.$image)
}
}
}
struct PhotoUIView_Previews: PreviewProvider {
static var previews: some View {
PhotoUIView()
}
}
So you can do is, creaate an array of images and add those selected images with name like image_0, image_1, and so on. And then add the images in the cell using the array eg a[i]
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.