I write some code to show an image dragged from other apps(such as web browser, photos, etc). I make a delegate to perform the drop.
If DropInfo has an image item, I will try to retrieve the data as uiimage by NSItemProvider.loadObject first. If errors occur during loading, I will ask DropInfo again whether it has a url item. If the answer is YES, I will try to retrieve URL by NSItemProvider.loadObject.
It means the second loadObject will be nested in the first one. When running the simulator, I find the second loadObject completion handler is never called which is supposed to be called when I try to retrieve URL. Do I miss anything?
import SwiftUI
import Combine
struct ContentView: View{
#State private var img: UIImage?
var body: some View{
Image(uiImage: img != nil ? img! : UIImage(systemName: "photo")!)
.resizable()
.scaledToFit()
.onDrop(of: [.image], delegate: ImageDropController(img: $img))
}
}
class ImageDropController: DropDelegate{
#Binding var img: UIImage?
init(img: Binding<UIImage?>) {
_img = img
}
func performDrop(info: DropInfo) -> Bool {
if getImgFromImage(info: info){
return true
}else{
return false
}
}
func getImgFromImage(info: DropInfo) -> Bool{
guard info.hasItemsConforming(to: [.image]) else {
return getImgFromUrl(info: info)
}
createImgFromImage(from: info.itemProviders(for: [.image]).first!,info: info)
return true
}
func createImgFromImage(from provider: NSItemProvider, info: DropInfo){
provider.loadObject(ofClass: UIImage.self) { image, error in
var unwrappedImage: UIImage?
if let error = error {
print("unwrapImage failed: ", error.localizedDescription)
_ = self.getImgFromUrl(info: info)
} else {
unwrappedImage = image as? UIImage
}
if let image = unwrappedImage{
DispatchQueue.main.async {
self.img = image
}
}
}
}
func getImgFromUrl(info: DropInfo) -> Bool{
guard info.hasItemsConforming(to: [.url]) else {
return false
}
createImgFromUrl(from: info.itemProviders(for: [.url]).first!)
return true
}
private func createImgFromUrl(from provider: NSItemProvider){
var fetchUrl: URL?
print("create from url")
_ = provider.loadObject(ofClass: URL.self) { url, error in
print("nested handler") <<------- never be called
if let error = error {
print("unwrapUrl failed: ", error.localizedDescription)
} else {
fetchUrl = url
print("url", fetchUrl?.description)
}
if let url = fetchUrl{
// Do some data fetch work using url
}
}
}
}
Related
I'm creating simple CRUD operations in an app and I came across a hairy quirk, given SwiftUI's Image design.
I'm trying to upload an image to Firebase, except I need to convert an Image to a UIImage, and further into Data.
Here's my code:
Image Uploader
import UIKit
import Firebase
import FirebaseStorage
import SwiftUI
struct ImageUploader {
static func uploadImage(with image: Data, completion: #escaping(String) -> Void) {
let imageData = image
let fileName = UUID().uuidString
let storageRef = Storage.storage().reference(withPath: "/profile_images/\(fileName)")
storageRef.putData(imageData, metadata: nil) { (metadata, error) in
if let error = error {
print("Error uploading image to Firebase: \(error.localizedDescription)")
return
}
print("Image successfully uploaded to Firebase!")
}
storageRef.downloadURL { url, error in
guard let imageUrl = url?.absoluteString else { return }
completion(imageUrl)
}
}
}
View Model
import SwiftUI
import Firebase
func uploadProfileImage(with image: Data) {
guard let uid = tempCurrentUser?.uid else { return }
let imageData = image
ImageUploader.uploadImage(with: imageData) { imageUrl in
Firestore.firestore().collection("users").document(uid).updateData(["profileImageUrl":imageUrl]) { _ in
print("Successfully updated user data")
}
}
}
ImagePicker
import SwiftUI
import PhotosUI
#MainActor
class ImagePicker: ObservableObject {
#Published var image: Image?
#Published var imageSelection: PhotosPickerItem? {
didSet {
if let imageSelection {
Task {
try await loadTransferrable(from: imageSelection)
}
}
}
}
func loadTransferrable(from imageSelection: PhotosPickerItem?) async throws {
do {
if let data = try await imageSelection?.loadTransferable(type: Data.self) {
if let uiimage = UIImage(data: data) {
self.image = Image(uiImage: uiimage)
}
}
} catch {
print("Error: \(error.localizedDescription)")
}
}
}
View
if let image = imagePicker.image {
Button {
viewModel.uploadProfileImage(with: image)
} label: {
Text("Continue")
.font(.headline)
.foregroundColor(.white)
.frame(width: 340.0, height: 50.0)
.background(.blue)
.clipShape(Capsule())
.padding()
}
.shadow(radius: 10.0)
.padding(24.0)
}
As you can see, I have a problem in the view: Cannot convert value of type 'Image' to expected argument type 'Data', which makes sense. The images are all of type Data.
How can I do it?
don't use #Published var image: Image? in your class ImagePicker: ObservableObject, Image is a View and is for use in other Views.
Use #Published var image: UIImage? and adjust your code accordingly.
Then use image.pngData() to get the Data to upload.
This is my Json file and i don't understand how to fetch data and set
the Image in our SwiftUI code. please help me resolve this problem.
And this is My Model, is this model correct?
This is My API and wants to fetch only value images array
import Foundation
public struct BannerImages {
public let images: [String]
public init(images: [String]) {
self.images = images
}
}
try this approach to fetch your images and display them in a View:
import Foundation
import SwiftUI
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
VStack {
Text("Fetching the data...")
List (vm.images, id: \.self) { url in
AsyncImage(url: URL(string: url)) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 111, height: 111)
} placeholder: {
ProgressView()
}
}
}
.task {
await vm.getData()
}
}
}
class ViewModel: ObservableObject {
#Published var images = [String]()
func getData() async {
guard let url = URL(string: "apiurl") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
Task{#MainActor in
let results = try JSONDecoder().decode(APIResponse.self, from: data)
self.images = results.images
}
} catch {
print("---> error: \(error)")
}
}
}
struct APIResponse: Codable {
let images: [String]
}
1. First you need to have a variable with data type of Data like this: var imageData: Data?
2. Then you have to fetch the image data from the link in the array like this:
func getImageData() {
// Check image url isnt nill
guard imageUrl(your image url) != nil else {
return
}
// Download the data for the image
let url = URL(string: imageUrl(your image url)!)
if let url = url {
let session = URLSession.shared
let dataTask = session.dataTask(with: url) { data, response, error in
if error == nil {
DispatchQueue.main.async {
self.imageData = data
}
}
}
dataTask.resume()
}
}
3. Once this is done go to the view file where you want to display the image and create
let uiImage = UIImage(data: put the var in which you stored the image data in previous step ?? Data())
Image(uiImage: uiImage ?? UIImage())
Sorry for simple question, try to learn SwiftUI
My goal is to show alert then i can not load data from internet using .alert()
the problem is that my struct for error actually has data but it does not transfer to .alert()
debug shows that AppError struct fill in with error but then i try to check for nil or not it is always nil in .Appear()
PostData.swift
struct AppError: Identifiable {
let id = UUID().uuidString
let errorString: String
}
NetworkManager.swift
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
#Published var appError: AppError? = nil
func fetchGuardData() {
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decorder = JSONDecoder()
if let safeData = data {
do {
let results = try decorder.decode(Results.self, from: safeData)
DispatchQueue.main.sync {
self.posts = results.hits }
} catch {
self.appError = AppError(errorString: error.localizedDescription)
}
} else {
self.appError = AppError(errorString: error!.localizedDescription)
}
} else {
DispatchQueue.main.sync {
self.appError = AppError(errorString: error!.localizedDescription)
}
}
} //
task.resume()
} else {
self.appError = AppError(errorString: "No url response")
}
}
}
ContentView.swift
struct ContentView: View {
#StateObject var networkManager = NetworkManager()
#State var showAlert = false
var body: some View {
NavigationView {
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationTitle("H4NEWS")
}
.onAppear() {
networkManager.fetchGuardData()
if networkManager.appError != nil {
showAlert = true
}
}
.alert(networkManager.appError?.errorString ?? "no data found", isPresented: $showAlert, actions: {})
}
}
Probably when doing this check, the data fetch process is not finished yet.
if networkManager.appError != nil {
showAlert = true
}
So you should wait the network request finish to check if there is error or not.
If you sure there is error and just test this try this to see error:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if networkManager.appError != nil {
showAlert = true
}
}
To handle better this situation you can pass a closure your fetchGuardData function and handle your result and error inside it.
or you can use .onChange for the listen the changes of appError.
.onChange(of: networkManager.appError) { newValue in }
Seems like there isn't many examples of using Google MobileAdsSDK 8.0 (iOS) with SwiftUI.
So far I have a class Interstitial
import GoogleMobileAds
import UIKit
final class Interstitial:NSObject, GADFullScreenContentDelegate{
var interstitial:GADInterstitialAd!
override init() {
super.init()
LoadInterstitial()
}
func LoadInterstitial(){
let req = GADRequest()
GADInterstitialAd.load(withAdUnitID: "...", request: req) { ad, error in
self.interstitial = ad
self.interstitial.fullScreenContentDelegate = self
}
}
func showAd(){
if self.interstitial != nil {
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
LoadInterstitial()
}
}
In my SwiftUI view i create a local variable Interstitial, and when an action is performed I call the showAd() function however when the ad displays it stops the code immediately following the showAd() call from running. So I think I need to somehow call showAd() and once the ad is dismissed then perform the remainder of my code in the view. As you can see above the Interstitial class is the delegate, but how do I "alert" my SwiftUI view that the ad was dismissed so I can execute the rest of the code? Below is my View.
import SwiftUI
struct MyView: View {
#Environment(\.managedObjectContext) var managedObjectContext
var interstitial : Interstitial = Interstitial()
var body: some View {
VStack{
//... Display content
}
.navigationBarItems(trailing:
HStack{
Button(action: actionSheet) {
Image(systemName: "square.and.arrow.up")
}
}
)
}
func showAd(){
interstitial.showAd()
}
func actionSheet() {
showAd()
let data = createPDF()
let temporaryFolder = FileManager.default.temporaryDirectory
let fileName = "export.pdf"
let temporaryFileURL = temporaryFolder.appendingPathComponent(fileName)
do {
try data.write(to: temporaryFileURL)
let av = UIActivityViewController(activityItems: [try URL(resolvingAliasFileAt: temporaryFileURL)], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
} catch {
print(error)
}
}
}
By adding an excaping closure, you pass a function and perform the needed actions.
final class InterstitialAd: NSObject, GADFullScreenContentDelegate {
var completion: () -> Void
var interstitial: GADInterstitialAd!
init(completion: #escaping () -> Void) {
self.completion = completion
super.init()
LoadInterstitialAd()
}
func LoadInterstitialAd() {
let req = GADRequest()
GADInterstitialAd.load(withAdUnitID: Constants.AdmobIDs.saveImageBlockID, request: req) { ad, error in
self.interstitial = ad
self.interstitial.fullScreenContentDelegate = self
}
}
func show() {
if self.interstitial != nil {
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
LoadInterstitialAd()
completion()
}
}
I found why the cache prompt will not be shown.
If I use the ImageView directly on the ContentView, the cache prompt will not show.
If I wrap the ImageView with a View, then use the wrapper view on the ContentView, the cache prompt will show.
Here is the working code in the ContentView.swift
struct ContentView: View {
var links =
[NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-0.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-1.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-2.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-3.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-4.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-5.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-6.jpg")]
var body: some View {
List(links) { news in
// working
NewsListItemView(item: news)
// not working
//NewsImageView(urlString: news.urlString)
}
}
}
This is the NewsListItemView which is just a wrapper
struct NewsListItemView: View {
var item: NewsItem
var body: some View {
NewsImageView(urlString: item.urlString)
}
}
This is my cache prompt location.
NewsImageViewModel.swift
class NewsImageViewModel: ObservableObject {
static var placeholder = UIImage(named: "NewsIcon.png")
#Published var image: UIImage?
var urlString: String?
init(urlString: String) {
self.urlString = urlString
loadImage()
}
func loadImage() {
if loadImageFromCache() {
return
}
loadImageFromURL()
}
func loadImageFromCache() -> Bool {
guard let cacheIamge = TemporaryImageCache.getShared()[urlString!] else {
return false
}
print("load from cache")
self.image = cacheIamge
return true
}
func loadImageFromURL() {
print("load from url")
guard let urlString = urlString else {
return
}
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url, completionHandler: getResponseFromURL(data:response:error:))
task.resume()
}
func getResponseFromURL(data: Data?, response: URLResponse?, error: Error?) {
guard error == nil else {
print("Error \(error!)")
return
}
guard data != nil else {
print("No founded data")
return
}
DispatchQueue.main.async {
guard let loadedImage = UIImage(data: data!) else {
print("Not supported data ")
return
}
self.image = loadedImage
TemporaryImageCache.getShared().cache.setObject(loadedImage, forKey: self.urlString! as NSString)
}
}
}
NewsImageView.swift
import SwiftUI
struct NewsImageView: View {
#ObservedObject var model: NewsImageViewModel
init(urlString: String) {
model = NewsImageViewModel(urlString: urlString)
}
var body: some View {
Image(uiImage: model.image ?? NewsImageViewModel.placeholder!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100, alignment: .center)
}
}
NewsListItemView.swift
struct NewsListItemView: View {
var item: NewsItem
var body: some View {
NewsImageView(urlString: item.urlString)
}
}
This is ImageCache.swift
protocol ImageCache {
subscript(_ urlString: String) -> UIImage? {get set }
}
struct TemporaryImageCache: ImageCache {
subscript(urlString: String) -> UIImage? {
get {
cache.object(forKey: urlString as NSString)
}
set {
newValue == nil ? cache.removeObject(forKey: urlString as NSString) : cache.setObject(newValue!, forKey: urlString as NSString)
}
}
var cache = NSCache<NSString, UIImage>()
}
extension TemporaryImageCache {
private static var shared = TemporaryImageCache()
static func getShared() -> TemporaryImageCache {
return shared
}
}
This is NewsItem.swift
struct NewsItem: Identifiable {
var id = UUID()
var urlString: String
}