image is not loading on swiftui collectionview - swift

I'm trying to load an image but the image is not loading.
I setup my image loader
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private let url: URL
private var cancellable: AnyCancellable?
init(url: URL) {
self.url = url
}
deinit {
cancellable?.cancel()
}
func load() {
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.assign(to: \.image, on: self)
}
func cancel() {
cancellable?.cancel()
}
}
and then AsyncImage struct
struct AsyncImage<Placeholder: View>: View {
#ObservedObject private var loader: ImageLoader
private let placeholder: Placeholder?
init(url: URL, placeholder: Placeholder? = nil) {
loader = ImageLoader(url: url)
self.placeholder = placeholder
}
var body: some View {
image
.onAppear(perform: loader.load)
.onDisappear(perform: loader.cancel)
}
private var image: some View {
placeholder
}
}
struct ProductSearhView: View {
#ObservedObject var model: SearchResultViewModel
var body: some View{
NavigationView {
List {
ForEach(0 ..< Global.productArry.count) { value in
CollectionView(model: self.model, data: Global.productArry[value])
}
}.navigationBarTitle("CollectionView")
}
}
}
struct CollectionView: View {
#ObservedObject var model: SearchResultViewModel
let data: Product
var body: some View {
VStack {
HStack {
Spacer()
AsyncImage(url: URL(string: self.data.productImageUrl)!, placeholder: Text("Loading ...")
).aspectRatio(contentMode: .fit)
Spacer()
}
HStack {
Spacer()
Text(self.data.name)
Spacer()
}
}.onAppear(perform:thisVal)
}
func thisVal (){
print(self.data.productImageUrl)
// https://i5.walmartimages.com/asr/113d660f-8b57-4ab8-8cfa-a94f11b121aa_1.f6bad4b281983e164dcf0a160571e886.jpeg?odnHeight=180&odnWidth=180&odnBg=ffffff
}
}
Everything else works fine except the image loading. The image only shows the placeholder and not the image.
Sample image url to load is this
https://i5.walmartimages.com/asr/113d660f-8b57-4ab8-8cfa-a94f11b121aa_1.f6bad4b281983e164dcf0a160571e886.jpeg?odnHeight=180&odnWidth=180&odnBg=ffffff
What am I doing wrong?

It looks like you're missing an Image.
Where you have:
private var image: some View {
placeholder
}
You're only ever going to have a placeholder.
If you change it to be:
private var image: some View {
Group {
if loader.image != nil {
Image(uiImage: loader.image!)
.resizable()
} else {
placeholder
}
}
}
That should mean that if the ImageLoader has successfully loaded the image it'll show the Image with the loaded uiImage, otherwise it'll show the placeholder.

Related

Array Indexing occurs Fatal error: Index out of range

Problem
Hello. I'm studying SwiftUI.
I've tried to pick multiple photos from gallery using PHPickerController, and show up multiple views which represents each photo one by one. However, Fatal error occurs whenever I try to access any index of vm.images.
How could I solve this issue?
My source code is as follows
Solved
The problem comes from vm.images I thought that .onChange modifier operate after all images are saved into vm.images. But it didn't.
I solved this matter by adding if statement when calling PickerTabView; Quite Easy
// Added code
if let images = vm.images {
if images.count > 0 {
PickerTabView()
}
}
struct PickerTabView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
TabView {
if let images = vm.images{
ForEach(images, id: \.self) { image in
PickerSettingView(image: image)
}
}
}
.tabViewStyle(.page)
}
}
struct ImagesPicker: UIViewControllerRepresentable {
#Binding var selectedImages: [UIImage]?
//var selectionLimit: Int
//var filter: PHPickerFilter?
var itemProvider: [NSItemProvider] = []
func makeUIViewController(context: Context) -> some PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.selectionLimit = 20
configuration.filter = .images
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func makeCoordinator() -> Coordinator {
return ImagesPicker.Coordinator(parent: self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate, UINavigationControllerDelegate {
var parent: ImagesPicker
init(parent: ImagesPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
if !results.isEmpty {
parent.itemProvider = []
parent.selectedImages = []
}
parent.itemProvider = results.map(\.itemProvider)
loadImage()
}
private func loadImage() {
for itemProvider in parent.itemProvider {
if itemProvider.canLoadObject(ofClass: UIImage.self) {
itemProvider.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.sync {
if let image = image as? UIImage {
self.parent.selectedImages?.append(image)
}
}
}
}
}
}
}
}
struct PickerHomeView: View {
#EnvironmentObject var vm: ViewModel
#State private var isSelected = false
var body: some View {
NavigationView {
VStack {
NavigationLink("Tab View", isActive: $isSelected) {
PickerTabView()
}
.hidden()
HStack {
Button {
vm.showPicker()
} label: {
ButtonLabel(symbolName: "photo.fill", label: "Photos")
}
}
Spacer()
}
.sheet(isPresented: $vm.showPicker) {
ImagesPicker(selectedImages: $vm.images)
.ignoresSafeArea()
}
.onChange(of: vm.images, perform: { _ in
isSelected = true
})
}
}
}
struct PickerSettingView: View {
#EnvironmentObject var vm: ViewModel
var image: UIImage
let myImage = MyImage(category: Category.unCategorized)
#State private var selectedCategory: Category = Category.unCategorized
var body: some View {
VStack {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth:.infinity)
SwiftUI.Picker("Category Picker", selection: $selectedCategory) {
Text("Formal").tag(Category.formal)
Text("Casual").tag(Category.casual)
Text("Semi Formal").tag(Category.semiFormal)
}
.pickerStyle(.segmented)
.padding([.leading, .trailing], 16)
HStack {
Button {
if vm.selectedImage == nil {
vm.addMyImage(category: selectedCategory, image: image)
} else {
vm.updateSelected()
}
} label: {
ButtonLabel(symbolName: vm.selectedImage == nil ? "square.and.arrow.down.fill" :
"square.and.arrow.up.fill",
label: vm.selectedImage == nil ? "Save" : "Update")
}
}
}
}
}
class ViewModel: ObservableObject {
#Published var images: [UIImage]?
#Published var showPicker = false
}
I think the image in your loadImage, is not being added to the self.parent.selectedImages
when it is nil, that is, when images in your ViewModel is nil, as it is at the start. So whenever you try to access any index of the images array in your vm.images, the app crashes.
You could try this in your loadImage (note also .async) to append the images:
DispatchQueue.main.async {
if let image = image as? UIImage {
if self.parent.selectedImages == nil { self.parent.selectedImages = [] }
self.parent.selectedImages!.append(image)
}
}

Presenting two sheets in SwiftUI

I'm trying to present two sheets in SwiftUI. The first sheet (SecondScreen) opens up on the Main Page (tapping the Navigation Tool Bar Icon) and the second sheet is a ShareSheet which should pop up inside the SecondScreen as an option. I have used a Form to build the SecondScreen. In the Simulator and on my device, the ShareSheet doesn't appear. I hope this is just a bug and not something Apple doesn't allow without big UI changes.
I tried to open the ShareSheet, while having the SecondScreen as a .fullScreenCover., instead of .sheet but the button still doesn't react.
Example
import SwiftUI
struct ContentView: View {
#State var showMore: Bool = false
var body: some View {
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showMore.toggle()
}, label: {
Image(systemName: "ellipsis.circle")
})
.sheet(isPresented: $showMore, content: {
SecondScreen()
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct SecondScreen: View {
var body: some View {
NavigationView {
Form {
Section {
Button(action: {
ShareID (Info: "https://www.google.com")}, label: { Text("Share")
})
}
}
}
}
}
}
func ShareID(Info: String){
let infoU = Info
let av = UIActivityViewController(activityItems: [infoU], applicationActivities: nil)
UIApplication.shared.windows.first?
.rootViewController?.present(av, animated: true,
completion: nil)
}
Thank you!
this is another approach to popup your sheets, even works on my mac:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var showMore: Bool = false
var body: some View {
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showMore.toggle() }) {
Image(systemName: "ellipsis.circle")
}
.sheet(isPresented: $showMore) {
SecondScreen()
}
}
}
}
}
}
struct SecondScreen: View {
#State var shareIt = false
#State var info = "https://www.google.com"
var body: some View {
Button(action: {shareIt = true}) {
Text("Share")
}
.sheet(isPresented: $shareIt, onDismiss: {shareIt = false}) {
ShareSheet(activityItems: [info as Any])
}
}
}
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [Any]
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { }
}

How to search a custom text from textfield in webkit swiftUI

I am building a small browser in swift ui i have build a struct to represent the wkwebview and i want to enter any text in a textfield and search on the internet using wkwebview and a google query i tried to reinstantiate the Webview(web: nil, req: URLRequest(url: URL(string: searchText)!)) but its not working for me? how can i achieve this i am pretty new to SwiftUI.Please help?
struct Webview : UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
init(web: WKWebView?, req: URLRequest) {
self.webview = WKWebView()
self.request = req
}
func makeUIView(context: Context) -> WKWebView {
return webview!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func goBack(){
webview?.goBack()
}
func goForward(){
webview?.goForward()
}
func refresh(){
webview?.reload()
}
}
import SwiftUI
struct ContentView: View {
var webview = Webview(web: nil, req: URLRequest(url: URL(string: "https://www.apple.com")!))
#State private var searchText = ""
#State private var txt = ""
var body: some View {
ZStack {
HStack {
TextField("Search", text: $searchText,onCommit: {
print(searchText)
}
)
.keyboardType(.URL)
.frame(width: UIScreen.main.bounds.size.width * 0.75 )
}
}
webview
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {
webview.goBack()
}) {
Image(systemName: "arrow.left")
}
Spacer()
Button(action: {
webview.goForward()
}) {
Image(systemName: "arrow.right")
}
Spacer()
Button(action: {
webview.refresh()
}) {
Image(systemName: "arrow.clockwise")
}
}
}
}
}
The way that you have things set up right now (attempting to store a reference to a UIViewRepresentable) is going to lead to problems later on (like this, in fact).
I suggest you set up an interim object of some sort that can be used to store data and communicate imperatively to the WKWebView while still retaining the ability to lay things out declaratively. In my example, WebVewManager is that interim object that both the parent view and the UIViewRepresentable have access to:
class WebViewManager : ObservableObject {
var webview: WKWebView = WKWebView()
init() {
webview.load(URLRequest(url: URL(string: "https://apple.com")!))
}
func searchFor(searchText: String) {
if let searchTextNormalized = searchText.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "https://google.com/search?q=\(searchTextNormalized)") { self.loadRequest(request: URLRequest(url: url))
}
}
func loadRequest(request: URLRequest) {
webview.load(request)
}
func goBack(){
webview.goBack()
}
func goForward(){
webview.goForward()
}
func refresh(){
webview.reload()
}
}
struct Webview : UIViewRepresentable {
var manager : WebViewManager
init(manager: WebViewManager) {
self.manager = manager
}
func makeUIView(context: Context) -> WKWebView {
return manager.webview
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct ContentView: View {
#StateObject private var manager = WebViewManager()
#State private var searchText = ""
#State private var txt = ""
var body: some View {
ZStack {
HStack {
TextField("Search", text: $searchText,onCommit: {
print(searchText)
manager.searchFor(searchText: searchText)
})
.keyboardType(.URL)
.frame(width: UIScreen.main.bounds.size.width * 0.75 )
}
}
Webview(manager: manager)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {
manager.goBack()
}) {
Image(systemName: "arrow.left")
}
Spacer()
Button(action: {
manager.goForward()
}) {
Image(systemName: "arrow.right")
}
Spacer()
Button(action: {
manager.refresh()
}) {
Image(systemName: "arrow.clockwise")
}
}
}
}
}
I'm doing some simple percent encoding on the search string -- you may need to do more testing to make sure that google always accepts the search query, but this looks like it's working to me.

SwiftUI Why Can't pass a publisher between Views?

I just want to do some test like this ↓
Create one publisher from first view
Pass it to second view
Bind the publisher with some property in second view and try to show it on screen
The code is ↓ (First View)
struct ContentView: View {
let publisher = URLSession(configuration: URLSessionConfiguration.default)
.dataTaskPublisher(for: URLRequest(url: URL(string: "https://v.juhe.cn/joke/content/list.php?sort=asc&page=&pagesize=&time=1418816972&key=aa73ebdd8672a2b9adc9dbb2923184c8")!))
.map(\.data.description)
.replaceError(with: "Error!")
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
var body: some View {
NavigationView {
List {
NavigationLink(destination: ResponseView(publisher: publisher)) {
Text("Hello, World!")
}
}.navigationBarTitle("Title", displayMode: .inline)
}
}
}
(Second View)
struct ResponseView: View {
let publisher: AnyPublisher<String, Never>
#State var content: String = ""
var body: some View {
HStack {
VStack {
Text(content)
.font(.system(size: 12))
.onAppear { _ = self.publisher.assign(to: \.content, on: self) }
Spacer()
}
Spacer()
}
}
}
But the code is not working. The request failed with message blow ↓
2020-11-11 11:08:04.657375+0800 PandaServiceDemo[83721:1275181] Task <6B53516E-5127-4C5E-AD5F-893F1AEE77E8>.<1> finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=https://v.juhe.cn/joke/content/list.php?sort=asc&page=&pagesize=&time=1418816972&key=aa73ebdd8672a2b9adc9dbb2923184c8, NSLocalizedDescription=cancelled, NSErrorFailingURLKey=https://v.juhe.cn/joke/content/list.php?sort=asc&page=&pagesize=&time=1418816972&key=aa73ebdd8672a2b9adc9dbb2923184c8}
Can someone tell me what happened and what is the right approach to do this?
The issue here is, the Subscription isn't stored anywhere. You have to store it in a AnyCancellable var and retain the subscription.
Use .print() operator whenever you are debugging combine related issues. I find it really useful.
The right approach is to extract the publisher and subscription into an ObservableObject and inject it into the View or use #StateObject
class DataProvider: ObservableObject {
#Published var content: String = ""
private var bag = Set<AnyCancellable>()
private let publisher: AnyPublisher<String, Never>
init() {
publisher = URLSession(configuration: URLSessionConfiguration.default)
.dataTaskPublisher(for: URLRequest(url: URL(string: "https://v.juhe.cn/joke/content/list.php?sort=asc&page=&pagesize=&time=1418816972&key=aa73ebdd8672a2b9adc9dbb2923184c8")!))
.map(\.data.description)
.print()
.replaceError(with: "Error!")
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
func loadData() {
publisher.assign(to: \.content, on: self).store(in: &bag)
}
}
struct ContentView: View {
#StateObject var dataProvider = DataProvider()
var body: some View {
NavigationView {
List {
NavigationLink(destination: ResponseView(dataProvider: dataProvider)) {
Text("Hello, World!")
}
}.navigationBarTitle("Title", displayMode: .inline)
}
}
}
struct ResponseView: View {
let dataProvider: DataProvider
var body: some View {
HStack {
VStack {
Text(dataProvider.content)
.font(.system(size: 12))
.onAppear {
self.dataProvider.loadData()
}
Spacer()
}
Spacer()
}
}
}
Please note that we have used #StateObject to make sure that DataProvider instance does not get destroyed when the view updates.
You need to store the subscription, otherwise it would be de-initialized and automatically cancelled.
Typically, this is done like this:
var cancellables: Set<AnyCancellable> = []
// ...
publisher
.sink {...}
.store(in: &cancellables)
So, you can create a #State property like the above, or you can use .onReceive:
let publisher: AnyPublisher<String, Never>
var body: some View {
HStack {
// ...
}
.onReceive(publisher) {
content = $0
}
}
You should be careful with the above approaches, since if ResponseView is ever re-initialized, it would get a copy of the publisher (most publishers are value-types), so it would start a new request.
To avoid that, add .share() to the publisher:
let publisher = URLSession(configuration: URLSessionConfiguration.default)
.dataTaskPublisher(...)
//...
.share()
.eraseToAnyPublisher()
In terms of SwiftUI, you are doing something fundamentally wrong: creating the publisher from the View. This means a new publisher will be created every time ContentView is instantiated, and for all means and purposes this can happen a lot of times, SwiftUI makes no guarantees a View will be instantiated only once.
What you need to do is to extract the published into some object, which is either injected from upstream, or managed by SwiftUI, via #StateObject.
Well there is 2 way for this Job: way one is better
Way 1:
import SwiftUI
struct ContentView: View {
#State var urlForPublicsh: URL?
var body: some View {
VStack
{
Text(urlForPublicsh?.absoluteString ?? "nil")
.padding()
Button("Change the Publisher") {urlForPublicsh = URL(string: "https://stackoverflow.com")}
.padding()
SecondView(urlForPublicsh: $urlForPublicsh)
}
.onAppear()
{
urlForPublicsh = URL(string: "https://www.apple.com")
}
}
}
struct SecondView: View {
#Binding var urlForPublicsh: URL?
var body: some View {
Text(urlForPublicsh?.absoluteString ?? "nil")
.padding()
}
}
Way 2:
import SwiftUI
class UrlForPublicshModel: ObservableObject
{
static let shared = UrlForPublicshModel()
#Published var url: URL?
}
struct ContentView: View {
#StateObject var urlForPublicshModel = UrlForPublicshModel.shared
var body: some View {
VStack
{
Text(urlForPublicshModel.url?.absoluteString ?? "nil")
.padding()
Button("Change the Publisher") {urlForPublicshModel.url = URL(string: "https://stackoverflow.com")}
.padding()
SecondView()
}
.onAppear()
{
urlForPublicshModel.url = URL(string: "https://www.apple.com")
}
}
}
struct SecondView: View {
#ObservedObject var urlForPublicshModel = UrlForPublicshModel.shared
var body: some View {
Text(urlForPublicshModel.url?.absoluteString ?? "nil")
.padding()
}
}

LazyVGrid, List, LazyStacks don't release views from memory?

I'm playing around with the new photo picker in SwiftUI 2 and I made a simple app to show the imported images in a LazyVGrid but when scrolling down, if I imported around 150 images the app finish all the memory and it crashes (Terminated due to memory issue).
I tried the same with a LazyVStack and List but they have the same problem, I was expecting lazy items to release all the cells that goes off screen from the memory but it doesn't look like it's working.
Is this a bug or am I doing something wrong?
Here's my code:
import SwiftUI
struct Media: Identifiable {
var id = UUID()
var image: Image
}
struct ContentView: View {
#State var itemProviders: [NSItemProvider] = []
#State private var showingPhotoPicker: Bool = false
let columns = [
GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 8)
]
#State var medias: [Media] = []
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(medias) { media in
media.image
.resizable()
.scaledToFill()
.frame(width: 100, height: 100, alignment: .center)
.clipped()
}
}
}
.navigationBarTitle("Images \(medias.count)")
.navigationBarItems(leading: Button(action: {
loadImages()
}, label: {
Text("Import \(itemProviders.count) images")
}), trailing: Button(action: {
showingPhotoPicker.toggle()
}, label: {
Image(systemName: "photo.on.rectangle.angled")
}))
.sheet(isPresented: $showingPhotoPicker) {
MultiPHPickerView(itemProviders: $itemProviders)
}
}
}
func loadImages() {
for item in itemProviders {
if item.canLoadObject(ofClass: UIImage.self) {
item.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.async {
guard let image = image as? UIImage else {
return
}
medias.append(Media(image: Image(uiImage: image)))
}
}
}
}
}
}
And the PhotoPickerView:
import SwiftUI
import PhotosUI
struct MultiPHPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) private var presentationMode
#Binding var itemProviders: [NSItemProvider]
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 0
let controller = PHPickerViewController(configuration: configuration)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController( _ uiViewController: PHPickerViewController, context: Context) {}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
#Environment(\.presentationMode) private var presentationMode
var parent: MultiPHPickerView
init( _ parent: MultiPHPickerView ) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss( animated: true )
self.parent.itemProviders = results.map(\.itemProvider)
}
}
}