When I run my code with breakpoint, I see the thumbnail image, but I cannot see the image in the application. Why ?
Using:
generateThumbnailRepresentations(url: currentFile.localPath)
.resizable()
.scaledToFit()
.frame(width: 60, height: 100)
.aspectRatio(contentMode: .fill)
QLThumbnailGenerator:
func generateThumbnailRepresentations(url: String) -> Image {
let pathURL = URL(fileURLWithPath: url)
var image: UIImage = UIImage()
// Set up the parameters of the request.
let size: CGSize = CGSize(width: 60, height: 100)
let scale = UIScreen.main.scale
// Create the thumbnail request.
let request = QLThumbnailGenerator.Request(fileAt: pathURL,
size: size,
scale: 1.0,
representationTypes: .thumbnail)
// Retrieve the singleton instance of the thumbnail generator and generate the thumbnails.
let generator = QLThumbnailGenerator.shared
generator.generateRepresentations(for: request) { (thumbnail, type, error) in
DispatchQueue.main.async {
if thumbnail == nil || error != nil {
// Handle the error case gracefully.
print("error \(error)")
} else {
// Display the thumbnail that you created.
image = thumbnail?.uiImage ?? UIImage(systemName: "photo")!
print("foo")
}
}
}
return Image(uiImage: image)
}
Related
I need to render a SwiftUI view into an image with opacity, so that empty space of the view would be transparent when I layer the image above some background.
I use this code for conversion:
func convertViewToData<V>(view: V, size: CGSize) -> UIImage? where V: View {
guard let rootVC = UIApplication.shared.windows.first?.rootViewController else {
return nil
}
let imageVC = UIHostingController(rootView: view.edgesIgnoringSafeArea(.all))
imageVC.view.frame = CGRect(origin: .zero, size: size)
rootVC.view.insertSubview(imageVC.view, at: 0)
let uiImage = imageVC.view.asImage(size: size)
imageVC.view.removeFromSuperview()
return uiImage
}
extension UIView {
func asImage(size: CGSize) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.opaque = false
return UIGraphicsImageRenderer(bounds: bounds, format: format).image { context in
layer.render(in: context.cgContext)
}
}
}
extension View{
func convertToImage(size: CGSize) -> UIImage?{
convertViewToData(view: self, size: size)
}
}
And this code to test the resulting image:
struct ContentView: View {
var view: some View{
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
}
var body: some View {
HStack{
view
Image(uiImage: view.convertToImage(size: .init(width: 200, height: 200))!)
}
.background(LinearGradient(stops: [.init(color: .green, location: 0), .init(color: .red, location: 1)], startPoint: .bottom, endPoint: .top))
}
}
This code produces two instances of the text: the one on the left is layered on the gradient background, and the one on the right is on the black background:
Clearly, the transparent parts are replaced by the black color in the image.
I figured out that the alpha information is discarded somewhere in the convertViewToData, but I was not able to find a way to preserve it.
I want to mask a linear gradient with a Image initialized with Image(uiImage: ...). This kind of masking can clearly be done with Images initialized as systemName but when done with a UIImage there is no "masking" performed. Note that I need to use UIImage because my intention is to use a generated qrcode as the mask.
Correctly Masked sf image:
Incorrectly masked UIImage:
Exmaple Code:
struct ContentView: View {
let string: String = "String"
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
let size: CGFloat = 150
func generateQRCode(string: String) -> UIImage? {
let data = Data(string.utf8)
filter.setValue(data, forKey: "inputMessage")
if let qrCodeImage = filter.outputImage {
if let qrCodeCGImage = self.context.createCGImage(qrCodeImage, from: qrCodeImage.extent) {
return UIImage(cgImage: qrCodeCGImage)
}
}
return nil
}
var body: some View {
VStack {
// sf image qr code
GradientRectangle()
.mask {
Image(systemName: "qrcode")
.resizable()
}
.frame(width: size, height: size)
// uiimage qr code
if let image = generateQRCode(string: string) {
GradientRectangle()
.mask {
Image(uiImage: image)
.interpolation(.none)
.resizable()
}
.frame(width: size, height: size)
}
}
}
}
struct GradientRectangle: View {
var body: some View {
Rectangle()
.fill(LinearGradient(colors: [.yellow, .orange], startPoint: .topLeading, endPoint: .bottomTrailing))
}
}
The generated qrCodeImage has no transparency (as you expected) but black& white, so it is just needed to convert it additionally into mask.
Tested with Xcode 13.2 / iOS 15.2
Here is fixed part of code:
func generateQRCode(string: String) -> UIImage? {
let data = Data(string.utf8)
filter.setValue(data, forKey: "inputMessage")
if let qrCodeImage = filter.outputImage {
let maskFilter = CIFilter.maskToAlpha()
maskFilter.setDefaults()
maskFilter.setValue(qrCodeImage, forKey:"inputImage")
if let maskImage = maskFilter.outputImage, let qrCodeCGImage = self.context.createCGImage(maskImage, from: maskImage.extent) {
return UIImage(cgImage: qrCodeCGImage)
}
}
return nil
}
I create a view that will become a snapshot to send to the Share Sheet. Unfortunately, I get a nil image the first time I click share. The second time, the snapshot image shows up fine.
struct AffirmationSharingView: View {
//saving this view to send to shareSheet
var viewToShare: some View {
ZStack{
Image("night4")
.resizable()
Text("affirmation here")
.foregroundColor(.white)
}
}
#State private var showShareSheet = false
#State var myImage: UIImage! = UIImage(named: "test")
var body: some View {
GeometryReader{gp in
ZStack{
Image("night4")
.resizable()
.frame(width: gp.size.width*0.9, height: gp.size.height*0.5 , alignment: .top)
Text("affirmation here")
Spacer()
HStack{
VStack{
Image("wigshareIcon")
.resizable()
.aspectRatio(1, contentMode:.fit)
.frame(width: gp.size.width*0.2, height: 60, alignment: .top)
Text("Share To Other Social Media")
}
.onTapGesture {
//save the image/affirmation combo to a UIImage to be sent to the share sheet
myImage = viewToShare.snapshot()
self.showShareSheet = true
}
}
.padding()
}
}
}
.onAppear(perform: getImage)
.sheet(isPresented: $showShareSheet, content: {
// let myImage3 = viewToShare.snapshot()
ShareView(activityItems: ["Rest Rise Grow App!",myImage]) //myImage!]) //[myImage as! Any]) //[data])
}
func getImage(){
self.myImage = viewToShare.snapshot()
if self.myImage != nil {
print("1sharesheet has some value")
print("1sharesheet equals \(myImage)")
} else {
print("Did not set 1screenshot")
}
}
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 are the logs:
2021-12-27 20:20:44.389453-0500 Rest Rise Grow[1672:423870] [Snapshotting] View (0x102168800, _TtGC7SwiftUI14_UIHostingViewGVS_15ModifiedContentGS1_GVS_6ZStackGVS_9TupleViewTGS1_VS_14LinearGradientVS_12_FrameLayout_GS1_VS_5ImageGVS_16_OverlayModifierGVS_10_ShapeViewGVS_13_StrokedShapeVVS_16RoundedRectangle6_Inset_VS_5Color___GVS_19_ConditionalContentVS_4TextS14_____GVS_11_ClipEffectS10___GVS_19_BackgroundModifierS12____) drawing with afterScreenUpdates:YES inside CoreAnimation commit is not supported.
2021-12-27 20:20:45.865260-0500 Rest Rise Grow[1672:424205] Metal API Validation Enabled
sharesheet has some value
sharesheet equals Optional(<UIImage:0x280e48360 anonymous {786, 831}>)
Rest_Rise_Grow/SettingsViewController.swift:833: Fatal error: Unexpectedly found nil while unwrapping an Optional value
2021-12-27 20:20:46.296762-0500 Rest Rise Grow[1672:423870] Rest_Rise_Grow/SettingsViewController.swift:833: Fatal error: Unexpectedly found nil while unwrapping an Optional value
So I'm trying to create a custom image picker something like instagram but way more basic. This is how I created the screen using this.
struct NewPostScreen: View {
#StateObject var manager = SelectNewPostScreenManager()
let columns = [GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1)]
var body: some View {
ScrollView {
VStack(spacing: 1) {
Image(uiImage: manager.selectedPhoto?.uiImage ?? UIImage(named: "placeholder-image")!)
.resizable()
.scaledToFit()
.frame(width: 350, height: 350)
.id(1)
LazyVGrid(columns: columns, spacing: 1) {
ForEach(manager.allPhotos) { photo in
Image(uiImage: photo.uiImage)
.resizable()
.scaledToFill()
.frame(maxWidth: UIScreen.main.bounds.width/3, minHeight: UIScreen.main.bounds.width/3, maxHeight: UIScreen.main.bounds.width/3)
.clipped()
.onTapGesture {
manager.selectedPhoto = photo
}
}
}
}
}
}
}
The UI looks good and everything but sometimes when I click an image using the tapGesture it gives me an incorrect selectedPhoto for my manager. Here is how my manager looks and how I fetch the photos from the library.
class SelectNewPostScreenManager: ObservableObject {
#Environment(\.dismiss) var dismiss
#Published var selectedPhoto: Photo?
#Published var allPhotos: [Photo] = []
init() {
fetchPhotos()
}
private func assetsFetchOptions() -> PHFetchOptions {
let fetchOptions = PHFetchOptions()
let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
fetchOptions.sortDescriptors = [sortDescriptor]
return fetchOptions
}
func fetchPhotos() {
print("Fetching Photos")
let options = assetsFetchOptions()
let allAssets = PHAsset.fetchAssets(with: .image, options: options)
DispatchQueue.global(qos: . background).async {
allAssets.enumerateObjects { asset, count, _ in
let imageManager = PHImageManager.default()
let targetSize = CGSize(width: 250, height: 250)
let options = PHImageRequestOptions()
options.isSynchronous = true
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { image, info in
guard let image = image else { return }
let photo = Photo(uiImage: image)
DispatchQueue.main.async {
self.allPhotos.append(photo)
}
}
}
}
}
}
This is how my photo object looks like as well.
struct Photo: Identifiable {
let id = UUID()
let uiImage: UIImage
}
I have no clue to why the tap gesture is not selecting the right item. Ive spent a couple of hours trying to figure out to why this is happening. I might just end up using the UIImagePickerController instead lol.
Anyways if someone can copy and paste this code into a new project of Xcode and run it on your actual device instead of the simulator. Let me know if its happening to you as well.
I was running it on an iPhone X.
The problem is that the image gesture are extending beyond your defined frame, I am sure there are many ways to fix this, but I solved it by adding the contentShape modifier
Please replace your image code with the following
Image(uiImage: photo.uiImage)
.resizable()
.scaledToFill()
.frame(width: UIScreen.main.bounds.width/3, height: UIScreen.main.bounds.width/3)
.clipped()
.contentShape(Path(CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width/3, height: UIScreen.main.bounds.width/3)))
.onTapGesture {
manager.selectedPhoto = photo
}
contentShape define the hit area for the gesture
SwiftUI has implicit animations with .animate(), and explicit ones using .withAnimation(). However, I can't figure out how to animate an image change:
struct ImageViewWidget : View {
#ObjectBinding var imageLoader: ImageLoader
init(imageURL: URL) {
imageLoader = ImageLoader(imageURL: imageURL)
}
var body: some View {
Image(uiImage:
(imageLoader.data.count == 0) ? UIImage(named: "logo-old")! : UIImage(data: imageLoader.data)!)
.resizable()
.cornerRadius(5)
.frame(width: 120, height:120)
}
}
This Image's uiImage argument is passed the old-logo (placeholder) if there's no data in imageLoader (a BindableObject), and replaces it with the correct one once that's asynchronously loaded:
class ImageLoader : BindableObject {
let didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(imageURL: URL) {
print("Image loader being initted!")
let url = imageURL
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
}
}.resume()
}
}
How can I animate this change, the moment where data.count stops being 0, and we have the image? say I want a fade out-in animation..
If you want to use explicit animations based on environment objects (or observable objects), you need to create some state in your view.
You can react to changes to an observable in your view using onReceive, and then modify your state using explicit animation.
struct ImageViewWidget: View {
#ObservedObject var imageLoader: ImageLoader
#State var uiImage: UIImage = UIImage(named: "logo-old")!
init(imageURL: URL) {
imageLoader = ImageLoader(imageURL: imageURL)
}
var body: some View {
Image(uiImage: uiImage)
.resizable()
.cornerRadius(5)
.frame(width: 120, height: 120)
.onReceive(imageLoader.$data) { data in
if data.count != 0 {
withAnimation {
self.uiImage = UIImage(data: data)!
}
}
}
}
}
You don't necessarily have to call .animate() or .withAnimation() because you are simply switching the images, you can use .transition() instead.
Assuming you have already successfully updated your image with your #ObjectBinding(#ObservedObject in Beta5), you can do this:
var body: some View {
if imageLoader.data.count == 0 {
Image(uiImage: UIImage(named: "logo-old")!)
.resizable()
.cornerRadius(5)
.frame(width: 120, height:120)
.transition(.opacity)
.animation(.easeInOut(duration:1))
} else {
Image(uiImage: UIImage(data: imageLoader.data)!)
.resizable()
.cornerRadius(5)
.frame(width: 120, height:120)
.transition(.opacity)
.animation(.easeInOut(duration:1))
}
}
or you can use a custom view modifier if you want to make the transition fancier:
struct ScaleAndFade: ViewModifier {
/// True when the transition is active.
var isEnabled: Bool
// fade the content view while transitioning in and
// out of the container.
func body(content: Content) -> some View {
return content
.scaleEffect(isEnabled ? 0.1 : 1)
.opacity(isEnabled ? 0 : 1)
//any other properties you want to transition
}
}
extension AnyTransition {
static let scaleAndFade = AnyTransition.modifier(
active: ScaleAndFade(isEnabled: true),
identity: ScaleAndFade(isEnabled: false))
}
and then inside your ImageViewWidget, add .transition(.scaleAndFade) to your Image as its view modifier