ERROR: IOSurfaceCreate 'RGBA' failed when calling requestImage from PHCachingImageManager - ios

I am using PHCachingImageManager to get thumbnail images for all images in the iOS photo library which are then rendered using a SwiftUI List. It seems to work for a small number of images (e.g. the six that load with the simulator), but given 1000s of images I'm incurring this error many times.
IIO_CreateIOSurfaceWithFormatAndBuffer:594: *** ERROR: IOSurfaceCreate 'RGBA' failed - clientAddress: 0x14d6a0000 allocSize: 0x00072000 size: 256 x 456 rb: 1024 [0x00000400] bpp: 4
What does this mean and what is the root cause? Does access to PHCachingImageManager need to be throttled down?
Below is a class similar to the one in my app that reproduces the issue on my iPhone SE2.
import Foundation
import SwiftUI
import UIKit
import Photos
let thumbnailSize = CGSize(width: 90, height: 90)
struct PhotoSelectView: View {
class ImageRowManager {
let thumbnailImageRequestOptions: PHImageRequestOptions
let cachingImageManager = PHCachingImageManager()
var rows: [SelectableImageRow] = []
init() {
let options = PHImageRequestOptions()
options.isSynchronous = true
options.resizeMode = .fast
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = false
self.thumbnailImageRequestOptions = options
}
func add(row: SelectableImageRow) {
self.rows.append(row)
}
}
struct SelectableImageRow: Hashable {
var rowIndex: Int
var images: [SelectableImage]
func hash(into hasher: inout Hasher) {
hasher.combine(self.rowIndex)
}
}
class SelectableImage: Hashable, ObservableObject {
#Published var image: UIImage? = nil
let id: String
private let asset: PHAsset
private let imageRowManager: ImageRowManager
init(asset: PHAsset, imageRowManager: ImageRowManager) {
self.id = asset.localIdentifier
self.asset = asset
self.imageRowManager = imageRowManager
self.loadImage()
}
func loadImage() {
DispatchQueue.global(qos: .background).async {
self.imageRowManager.cachingImageManager.requestImage(for: self.asset, targetSize: CGSize(width: 150, height: 150), contentMode: .aspectFill, options: self.imageRowManager.thumbnailImageRequestOptions) { (image, _) in
RunLoop.main.perform {
self.image = image
}
}
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
static func ==(lhs: SelectableImage, rhs: SelectableImage) -> Bool {
return lhs.id == rhs.id
}
}
let imageRowManager = ImageRowManager()
#State var selected = Set<SelectableImage>()
#State var grid: [SelectableImageRow] = []
var body: some View {
VStack {
VStack {
if !self.grid.isEmpty {
HStack {
Text("Pick images")
Spacer()
}
.padding(.leading)
.padding(.top)
ImagesScrollView(grid: self.$grid, selected: self.$selected)
Button(action: {
self.handleSelectButton()
}) {
Text("Select")
.foregroundColor(Color.black.opacity((self.selected.count != 0) ? 1 : 0.5))
.padding(.vertical,10)
.frame(width: UIScreen.main.bounds.width / 2)
.overlay(
Capsule(style: .continuous)
.stroke(Color.black.opacity((self.selected.count != 0) ? 1 : 0.5), style: StrokeStyle(lineWidth: 5))
)
}
.background(Color.white)
.padding(.bottom)
.disabled((self.selected.count != 0) ? false : true)
}
}
.frame(width: UIScreen.main.bounds.width - CGFloat(horizontalPadding), height: UIScreen.main.bounds.height / 1.5)
.background(Color.white)
.cornerRadius(12)
}
.background(Color.black.opacity(0.1)).edgesIgnoringSafeArea(.all)
.onAppear {
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
self.getAllImages()
} else {
print("Cannot access photo library")
}
}
}
}
private func handleSelectButton() {
print("selected images", self.selected)
}
private func getAllImages() {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let req = PHAsset.fetchAssets(with: .image, options: fetchOptions)
var rowIndex = 0
for i in stride(from: 0, to: req.count, by: gridItemWidth) {
var iteration : [SelectableImage] = []
for j in i..<i+gridItemWidth {
if j < req.count{
iteration.append(SelectableImage(asset: req[j], imageRowManager: self.imageRowManager))
}
}
let row = SelectableImageRow(rowIndex: rowIndex, images: iteration)
imageRowManager.add(row: row)
rowIndex += 1
}
self.grid = imageRowManager.rows
}
// Subviews
struct ImagesScrollView: View {
#Binding var grid: [SelectableImageRow]
#Binding var selected: Set<SelectableImage>
var body: some View {
List(self.grid, id: \.self) { row in
SelectableImageRowView(row: row, selected: self.$selected)
}
}
}
struct SelectableImageRowView: View {
var row: SelectableImageRow
#Binding var selected: Set<SelectableImage>
var body: some View {
HStack(spacing: 2) {
ForEach(row.images, id: \.self) { img in
SelectableImageCard(data: img, selected: self.$selected)
}
}
}
}
struct SelectableImageCard: View {
#ObservedObject var data: SelectableImage
#Binding var selected: Set<SelectableImage>
var body: some View {
ZStack {
Image(uiImage: self.image()).resizable()
if self.selected.contains(self.data) {
Image(systemName: "checkmark")
.resizable()
.padding(7)
.foregroundColor(.white)
.background(Color.blue)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 1))
.frame(width: 30, height: 30, alignment: .topTrailing)
.offset(x: 30, y: -28)
}
}
.frame(width: thumbnailSize.width, height: thumbnailSize.height)
.onTapGesture {
if !self.selected.contains(self.data) {
self.selected.insert(self.data)
} else{
self.selected.remove(self.data)
}
}
}
private func image() -> some UIImage {
if let image = self.data.image {
return image
} else {
return UIImage(systemName: "heart.fill")!
}
}
}
}

Related

UIView does not update after updateUIView is called on UIViewRepresentable

My UIView doesn't update after I change its properties in the updateUIView function. To test it, I click the button in the VStack which calls generateBarcodeData and changes the state of barcode.
I monitored the updateUIView function in BarCodeView and it definitely is getting called, however I don't see any changes on the simulator.
import SwiftUI
struct MainView: View {
let screenSize = UIScreen.main.bounds
let titleOffset = UIScreen.main.bounds.height/25
let mainModalOffset = UIScreen.main.bounds.height/10
#State private var barcode: String = "&723852390"
var body: some View {
ZStack() {
Color.blue.ignoresSafeArea()
VStack() {
Text("-|||||-")
.font(.system(.title, design: .rounded))
.fontWeight(.semibold)
.foregroundColor(Color.yellow)
.offset(y: titleOffset)
Spacer()
}
VStack() {
BarCodeView(barcode: $barcode)
.frame(height: screenSize.height/2.5)
.padding()
Button(action: {
generateBarcodeData()
})
{
Text("Reset Barcode")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
}
.padding()
.padding(.bottom, 150)
.frame(height: screenSize.height)
.background(Color.white)
.offset(y: mainModalOffset)
}
}
func generateBarcodeData() {
// let src128API = Src128API(username: self.username, password: self.password)
// src128API.getBarcode() { (barcodeData) in
// barcode = barcodeData
// print(barcodeData)
// }
let min: UInt32 = 100_000_000
let max: UInt32 = 999_999_999
let i = min + arc4random_uniform(max - min + 1)
barcode = String(i)
print(barcode)
}
}
extension UIImage {
convenience init?(barcode: String) {
let data = barcode.data(using: .ascii)
guard let filter = CIFilter(name: "CICode128BarcodeGenerator") else {
return nil
}
filter.setValue(data, forKey: "inputMessage")
guard let ciImage = filter.outputImage else {
return nil
}
self.init(ciImage: ciImage)
}
}
struct BarCodeView: UIViewRepresentable {
#Binding var barcode: String
func makeUIView(context: Context) -> UIImageView {
let imageView = UIImageView()
return imageView
}
func updateUIView(_ uiView: UIImageView, context: Context) {
uiView.image = UIImage(barcode: barcode)
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
I built the code to my iPhone and it worked on there. I guess it was just broken on the simulator.

SwiftUI LazyVStack overlapping images

I am trying to display two columns of images in a LazyVStack embedded in a scroll view however the the second row of images partially overlaps the row above. I'm not sure if this is an issue with the LazyVStack itself or an issue with the Photo.swift view.
The output looks like this
The two view files
ContentView.swift
struct ContentView: View {
#State private var image: Image?
#State private var showingCustomCamera = false
#State private var inputImage: UIImage?
#State private var photos: [UIImage] = []
func addImageToArray() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
let ciImage = CIImage(cgImage: inputImage.cgImage!)
let options = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: options)!
let faces = faceDetector.features(in: ciImage)
if let face = faces.first as? CIFaceFeature {
print("Found face at \(face.bounds)")
print(face.faceAngle)
print(face.hasSmile)
print(face.leftEyeClosed)
print(face.rightEyeClosed)
if face.leftEyeClosed {
print("Left Eye Closed \(face.leftEyePosition)")
}
if face.rightEyeClosed {
print("Right Eye Closed \(face.rightEyePosition)")
}
if face.hasSmile {
print("Person is smiling \(face.mouthPosition)")
}
}
photos.append(inputImage)
}
let columns = [
GridItem(.flexible(), spacing: 20),
GridItem(.flexible(), spacing: 20)
]
var body: some View {
NavigationView {
VStack{
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
AddPhoto(showCamera: $showingCustomCamera)
ForEach(photos, id: \.self) { photo in
PassportPhoto(img: photo)
}
}
.padding()
}
HStack {
Button(action: {
//
}, label: {
Image(systemName: "printer.fill.and.paper.fill")
Text("Print")
})
.padding()
.foregroundColor(.primary)
Button(action: {
//
}, label: {
Image(systemName: "externaldrive.fill.badge.icloud")
Text("Digital Upload")
})
.padding()
.foregroundColor(.primary)
}
}
.sheet(isPresented: $showingCustomCamera, onDismiss: addImageToArray) {
CustomCameraView(image: self.$inputImage)
}
.navigationTitle("Add Photos")
}
}
}
Photo.swift
struct Photo: View {
var img: UIImage
#State private var overlay: Bool = false
var body: some View {
GeometryReader { geometry in
VStack {
ZStack(alignment: .top) {
Image(uiImage: img)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: geometry.size.width * 1.29, alignment: .top)
.clipped()
.cornerRadius(10)
.onTapGesture {
self.overlay.toggle()
}
if overlay {
Template()
}
}
}
}
}
}
Anyone have any idea? I feel like I'm missing something obvious.
CustomCameraView.swift (as requested)
import SwiftUI
import AVFoundation
struct CustomCameraView: View {
#Binding var image: UIImage?
#State var didTapCapture: Bool = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack(alignment: .center) {
CustomCameraRepresentable(image: self.$image, didTapCapture: $didTapCapture)
.overlay(Template(),alignment: .center)
.overlay(
CaptureButtonView().onTapGesture {
self.didTapCapture = true
}
, alignment: .bottom)
.overlay(
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "multiply")
.scaleEffect(2)
.padding(20)
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
})
.foregroundColor(.white)
.padding()
, alignment: .topTrailing)
}
}
}
struct CustomCameraRepresentable: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var image: UIImage?
#Binding var didTapCapture: Bool
func makeUIViewController(context: Context) -> CustomCameraController {
let controller = CustomCameraController()
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ cameraViewController: CustomCameraController, context: Context) {
if(self.didTapCapture) {
cameraViewController.didTapRecord()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
let parent: CustomCameraRepresentable
init(_ parent: CustomCameraRepresentable) {
self.parent = parent
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
parent.didTapCapture = false
if let imageData = photo.fileDataRepresentation() {
parent.image = UIImage(data: imageData)
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
class CustomCameraController: UIViewController {
var image: UIImage?
var captureSession = AVCaptureSession()
var backCamera: AVCaptureDevice?
var frontCamera: AVCaptureDevice?
var currentCamera: AVCaptureDevice?
var photoOutput: AVCapturePhotoOutput?
var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
//DELEGATE
var delegate: AVCapturePhotoCaptureDelegate?
func didTapRecord() {
let settings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: settings, delegate: delegate!)
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
setupCaptureSession()
setupDevice()
setupInputOutput()
setupPreviewLayer()
startRunningCaptureSession()
}
func setupCaptureSession() {
captureSession.sessionPreset = AVCaptureSession.Preset.photo
}
func setupDevice() {
let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera],
mediaType: AVMediaType.video,
position: AVCaptureDevice.Position.unspecified)
for device in deviceDiscoverySession.devices {
switch device.position {
case AVCaptureDevice.Position.front:
self.frontCamera = device
case AVCaptureDevice.Position.back:
self.backCamera = device
default:
break
}
}
self.currentCamera = self.backCamera
}
func setupInputOutput() {
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: currentCamera!)
captureSession.addInput(captureDeviceInput)
photoOutput = AVCapturePhotoOutput()
photoOutput?.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])], completionHandler: nil)
captureSession.addOutput(photoOutput!)
} catch {
print(error)
}
}
func setupPreviewLayer()
{
let rect = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width * 1.29)
self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait
self.cameraPreviewLayer?.frame = rect
self.view.layer.insertSublayer(cameraPreviewLayer!, at: 0)
}
func startRunningCaptureSession(){
captureSession.startRunning()
}
}
struct CaptureButtonView: View {
#State private var animationAmount: CGFloat = 1
var body: some View {
Image(systemName: "camera").font(.largeTitle)
.padding(30)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.red)
.scaleEffect(animationAmount)
.opacity(Double(2 - animationAmount))
.animation(Animation.easeOut(duration: 1)
.repeatForever(autoreverses: false))
)
.padding(.bottom)
.onAppear
{
self.animationAmount = 2
}
}
}
You shouldn’t use GeometryReader from within the ScrollView, it will create all sort of mess for you. Instead define it at top level just under VStack, and pass the proxy down to Photo view to set Frame.
Check the code below-:
import SwiftUI
struct Test1: View {
#State private var image: Image?
#State private var showingCustomCamera = false
#State private var inputImage: UIImage?
#State private var photos: [UIImage] = []
func addImageToArray() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
let ciImage = CIImage(cgImage: inputImage.cgImage!)
let options = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: options)!
let faces = faceDetector.features(in: ciImage)
if let face = faces.first as? CIFaceFeature {
print("Found face at \(face.bounds)")
print(face.faceAngle)
print(face.hasSmile)
print(face.leftEyeClosed)
print(face.rightEyeClosed)
if face.leftEyeClosed {
print("Left Eye Closed \(face.leftEyePosition)")
}
if face.rightEyeClosed {
print("Right Eye Closed \(face.rightEyePosition)")
}
if face.hasSmile {
print("Person is smiling \(face.mouthPosition)")
}
}
photos.append(inputImage)
}
let columns =
[GridItem(.flexible(),spacing: 20),
GridItem(.flexible(),spacing: 20)]
var body: some View {
NavigationView {
VStack{
GeometryReader { geometry in
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
// AddPhoto(showCamera: $showingCustomCamera) // Uncomment in your case
ForEach(0..<50, id: \.self) { photo in
Photo(img: "ABC", proxy: geometry) // Pass photo as you were doing
}
}
.padding()
}
}
HStack {
Button(action: {
//
}, label: {
Image(systemName: "printer.fill.and.paper.fill")
Text("Print")
})
.padding()
.foregroundColor(.primary)
Button(action: {
//
}, label: {
Image(systemName: "externaldrive.fill.badge.icloud")
Text("Digital Upload")
})
.padding()
.foregroundColor(.primary)
}
}
.sheet(isPresented: $showingCustomCamera, onDismiss: addImageToArray) {
// CustomCameraView(image: self.$inputImage)
}
.navigationTitle("Add Photos")
}
}
}
struct Photo: View {
var img: String
var proxy:GeometryProxy
#State private var overlay: Bool = false
var body: some View {
// GeometryReader { geometry in
VStack {
ZStack(alignment: .top) {
Image(img)
.resizable()
.aspectRatio(contentMode: .fill)
// .frame(width: 170, height: 200)
.frame(width: proxy.size.width * 0.4, height: proxy.size.width * 0.5, alignment: .top)
.clipped()
.cornerRadius(10)
.onTapGesture {
self.overlay.toggle()
}
if overlay {
// Template()
}
}
}
//}
}
}

Does LazyHStack (SwiftUI) release memory for views out of screen?

I am trying to build a simple horizontal list of images. The images are loaded asynchronously, nothing fancy so far.
The main issue is that when scrolling and loading new images, the memory increases and never gets released. In case the images are really big, or the list of items very long, the app can crash while scrolling.
Shouldn't the LazyHStack do some memory management of views? I can not imagine this is not resolved so far, since it seems to me a very basic "feature".
The code (nothing special):
struct ThumbnailsCollectionView: View {
var imageService = ImageService.shared
#Binding var selectedIndex: Int
#State var image: UIImage?
var body: some View {
ScrollView([.horizontal]){
ScrollViewReader { reader in
LazyHStack(alignment: .bottom, spacing: 5){
ForEach((0...imageService.imageCount()-1), id: \.self) { index in
ThumbnailImageView(index: index)
}
}
}.padding(.vertical, 10)
}
}
}
struct ThumbnailImageView: View {
var imageService = ImageService.shared
var index: Int
#State var image: UIImage? = nil
var body: some View {
return Button(action: {
}) {
if let uiImage = image {
Image(uiImage: uiImage).resizable()
.aspectRatio(contentMode: .fit)
.zIndex(5)
}
else {
Rectangle().frame(width: 50, height: 50, alignment: .center)
}
}
.onAppear() {
imageService.thumbnail(at: index) { (image) in
self.image = image
}
}
}
}
ImageServiceCache
class ImageService {
static let shared = ImageService()
var thumbnailCache: NSCache<NSNumber, UIImage> = {
let cache = NSCache<NSNumber,UIImage>()
cache.countLimit = 10
return cache
}()
var urls:[URL] = {
var urls = [URL]()
for i in 0...100 {
let index = i % 4
urls.append(Bundle.main.url(forResource: "\(index)", withExtension: "jpg")!)
}
return urls
}()
func imageCount() -> Int {
return urls.count
}
func thumbnail(at index: Int, completion:#escaping (UIImage)->()) {
if let imageFromCache = thumbnailCache.object(forKey: NSNumber(integerLiteral: index)) {
return completion(imageFromCache)
}
DispatchQueue.global().async {
let image = UIImage(contentsOfFile: self.urls[index].path)!
self.thumbnailCache.setObject(image, forKey: NSNumber(integerLiteral: index))
DispatchQueue.main.async {
completion(image)
}
}
}
}

SwiftUI #ObservedObject does not get updated on Async fetch of the image

I have an app that fetches a list of items with images URL's from remote API, and then it has to fetch an image per item from given url inside that item's position.
The problem is that when scrolling up and down and thus removing list items from view and moving them back into view they do show up. However on initial load they stay in "loading" status forever until moved out and in.
My code:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var artObjectStore: ArtObjectStore
#State private var pageCount = 1
#State private var tappedLink: String? = nil
#Environment(\.imageCache) var cache: ImageCache
var body: some View {
NavigationView {
Form {
Section(header: Text("Art")) {
List {
ForEach(artObjectStore.artObjects, id: \.self) { artObject in
self.link(for: artObject)
}
Button(action: loadMore) {
Text("")
}
.onAppear {
DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 10)) {
self.loadMore()
}
}
}
}
}
.navigationBarTitle("Art objects")
}
.onAppear(perform: loadMore)
}
func loadMore() {
pageCount += 1
artObjectStore.loadMore(pageCount)
}
private func link(for artObject: ArtObject) -> some View {
let selection = Binding(get: { self.tappedLink },
set: {
UIApplication.shared.endEditing()
self.tappedLink = $0
})
return NavigationLink(destination: DetailView(artObject: artObject, cache: self.cache),
tag: artObject.id,
selection: selection) {
HStack(alignment: .center) {
VStack(alignment: .leading){
Text("\(artObject.title)").font(.system(size: 12))
Text("\(artObject.principalOrFirstMaker)").font(.system(size: 9)).foregroundColor(.gray)
}
Spacer()
AsyncImage(
url: URL(string: artObject.headerImage.url)!,
cache: self.cache,
width: 200,
height: 50
)
}
}
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Image container:
import SwiftUI
import Combine
import Foundation
class ImageLoader: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
private var cancellable: AnyCancellable?
#Published var image: UIImage? {
willSet {
objectWillChange.send()
}
}
private let url: URL
private var cache: ImageCache?
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancellable?.cancel()
}
private func cache(_ image: UIImage?) {
image.map { cache?[url] = $0 }
}
func load() {
if let image = cache?[url] {
self.image = image
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveOutput: { [weak self] in self?.cache($0) })
.receive(on: DispatchQueue.main)
.assign(to: \.image, on: self)
}
func cancel() {
cancellable?.cancel()
}
}
struct AsyncImage: View {
#ObservedObject private var loader: ImageLoader
private let width: CGFloat?
private let height: CGFloat?
#State var spin = false
init(url: URL, cache: ImageCache? = nil, width: CGFloat? = nil, height: CGFloat? = nil) {
loader = ImageLoader(url: url, cache: cache)
self.width = width
self.height = height
}
var body: some View {
image
.onAppear(perform: loader.load)
.onDisappear(perform: loader.cancel)
}
private var image: some View {
Group {
if loader.image != nil {
Image(uiImage: loader.image!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: width, height: height)
} else {
Image("loadingCircle")
.resizable()
.frame(width: 20, height: 20)
.rotationEffect(.degrees(spin ? 360 : 0))
.animation(Animation.linear(duration: 0.8).repeatForever(autoreverses: false))
.onAppear() {
self.spin.toggle()
}
}
}
}
}
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
struct TemporaryImageCache: ImageCache {
private let cache = NSCache<NSURL, UIImage>()
subscript(_ key: URL) -> UIImage? {
get { cache.object(forKey: key as NSURL) }
set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
}
}
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}
I did try to add willSet on the image, that doesn't seem to work. Can you help me?
All below tested with Xcode 11.4 / iOS 13.4
Modified worked AsyncImage
I changed Group to VStack and it started updating
private var image: some View {
VStack { // << here !!
if loader.image != nil {
Modified worked image loader
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private var cancellable: AnyCancellable?
private let url: URL
private var cache: ImageCache?
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancellable?.cancel()
}
private func cache(_ image: UIImage?) {
self.image = image
image.map { cache?[url] = $0 }
}
func load() {
if let image = cache?[url] {
self.image = image
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.receive(on: DispatchQueue.main)
.replaceError(with: nil)
.sink(receiveValue: { [weak self] in self?.cache($0) })
}
func cancel() {
cancellable?.cancel()
}
}

Have 0th item in SwiftUI auto selected

I have data loaded into an HStack that is in a Scroll View in SwiftUI. Right now I have it coded where a user can tap on one of those items and have it selected. I'd like for the 0th item to already be selected upon load.
import SwiftUI
import Combine
import Contentful
struct moviesView : View {
#ObservedObject var fetcher = MovieFetcher()
#State var selectMovie: Movie? = nil
#Binding var show: Bool
var body: some View {
HStack(alignment: .bottom) {
if show {
ScrollView(.horizontal) {
Spacer()
HStack(alignment: .bottom, spacing: 30) {
ForEach(fetcher.movies, id: \.self) { item in
selectableRow(movie: item, selectMovie: self.$selectMovie)
}
.onAppear() {
self.selectMovie = self.movies.count > 0 ? self.movies.first! : nil
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding(.leading, 46)
.padding(.bottom, 26)
}
}
}
}
struct selectableRow : View {
var movie: Movie
#Binding var selectedMovie: Movie?
#State var initialImage = UIImage()
var body: some View {
ZStack(alignment: .center) {
if movie == selectedMovie {
Image("")
.resizable()
.frame(width: 187, height: 254)
.overlay(
RoundedRectangle(cornerRadius: 13)
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 182, height: 249)
.onAppear {
let urlString = "\(urlBase)\(self.movie.movieId).png?"
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
} else {
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 135, height: 179)
.onAppear {
let urlString = "\(urlBase)\(self.movie.movieId).png?"
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
}
}
.onTapGesture {
self.selectedMovie = self.movie
}
}
}
EDIT:
I've added the below suggestion but it's still now working properly. Maybe it's where I added the .onAppear?
So when I launch my app I see the 0th item is selected but when I tap on any item the view just reloads but the 0th item always stays selected.
Additional issue:
Also, my #ObservedObject var fetcher = MovieFetcher() in moviesView is called repeatedly.
Since you haven't given the full working code, I wasn't able to reproduce the issue you've mentioned. However, I'd suggest you move the .onAppear from ForEach to the HStack (see code below).
I couldn't reproduce the issue you specified.
var body: some View {
HStack(alignment: .bottom) {
if show {
ScrollView(.horizontal) {
Spacer()
HStack(alignment: .bottom, spacing: 30) {
ForEach(fetcher.movies, id: \.self) { item in
selectableRow(movie: item, selectedMovie: self.$selectMovie)
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding(.leading, 46)
.padding(.bottom, 26)
.onAppear() {
self.selectMovie = self.fetcher.movies.count > 0 ? self.fetcher.movies.first! : nil
}
}
}
}
In the struct moviesView, use the below code to auto select the first movie.
.onAppear() {
self.selectMovie = self.movies.count > 0 ? self.movies.first! : nil
}
Let me know if you have any other questions.

Resources