SwiftUI, CloudKit and Images - image

I'm really stumped by something I think that should be relatively easy, so i need a little bump in the right direction. I've searched in a lot of places and I get either the wrong information, or outdated information (a lot!).
I am working with Core Data and CloudKit to sync data between the user's devices. Images I save as CKAsset attached to a CKRecord. That works well. The problem is with retrieving the images. I need the images for each unique enitity (Game) in a list. So I wrote a method on my viewModel that retrieves the record with the CKAsset. This works (verified), but I have no idea how to get the image out and assign that to a SwiftUI Image() View. My current method returns a closure with a UIImage, how do I set that image to an Image() within a foreach. Or any other solution is appreciated. Musn't be that hard to get the image?
/// Returns the saved UIImage from CloudKit for the game or the default Image!
func getGameImageFromCloud(for game: Game, completion: #escaping (UIImage) -> Void ) {
// Every game should always have an id (uuid)!
if let imageURL = game.iconImageURL {
let recordID = CKRecord.ID(recordName: imageURL)
var assetURL = ""
CKContainer.default().privateCloudDatabase.fetch(withRecordID: recordID) { record, error in
if let error = error {
print(error.getCloudKitError())
return
} else {
if let record = record {
if let asset = record["iconimage"] as? CKAsset {
assetURL = asset.fileURL?.path ?? ""
DispatchQueue.main.async {
completion(UIImage(contentsOfFile: assetURL) ?? AppImages.gameDefaultImage)
}
}
}
}
}
} else {
completion(AppImages.gameDefaultImage)
}
}
This is the ForEach I want to show the Image for each game (but this needed in multiple places:
//Background Tab View
TabView(selection: $gamesViewModel.currentIndex) {
ForEach(gamesViewModel.games.indices, id: \.self) { index in
GeometryReader { proxy in
Image(uiImage: gamesViewModel.getGameImageFromCloud(for: gamesViewModel.games[index], completion: { image in
}))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: proxy.size.width, height: proxy.size.height)
.cornerRadius(1)
}
.ignoresSafeArea()
.offset(y: -100)
}
.onAppear(perform: loadImage)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.animation(.easeInOut, value: gamesViewModel.currentIndex)
.overlay(
LinearGradient(colors: [
Color.clear,
Color.black.opacity(0.2),
Color.white.opacity(0.4),
Color.white,
Color.systemPurple,
Color.systemPurple
], startPoint: .top, endPoint: .bottom)
)
.ignoresSafeArea()
TIA!

So, let's go... extract ForEach image dependent internals into subview, like (of course it is not testable, just idea):
ForEach(gamesViewModel.games.indices, id: \.self) { index in
GeometryReader { proxy in
GameImageView(model: gamesViewModel, index: index) // << here !!
.frame(width: proxy.size.width, height: proxy.size.height)
.cornerRadius(1)
//.onDisappear { // if you think about cancelling
// gamesViewModel.cancelLoad(for: index)
//}
}
.ignoresSafeArea()
.offset(y: -100)
}
.onAppear(perform: loadImage)
and now subview itself
struct GameImageView: View {
var model: Your_model_type_here
var index: Int
#State private var image: UIImage? // << here !!
var body: some View {
Group {
if let loadedImage = image {
Image(uiImage: loadedImage) // << here !!
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Text("Loading...")
}
}.onAppear {
model.getGameImageFromCloud(for: model.games[index]) { image in
self.image = image
}
}
}
}

For completion's sake, my own version:
struct GameImage: View {
var game: Game
#EnvironmentObject var gamesViewModel: GamesView.ViewModel
#State private var gameImage: UIImage?
var body: some View {
Group {
if let gameImage = gameImage {
Image(uiImage: gameImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
ZStack(alignment: .center) {
Image(uiImage: AppImages.gameDefaultImage)
.resizable()
.aspectRatio(contentMode: .fill)
ProgressView()
.foregroundColor(.orange)
.font(.title)
}
}
}.onAppear {
gamesViewModel.getGameImageFromCloud(for: game) { image in
self.gameImage = image
}
}
}
}

Related

Progress View doesn't show on second + load when trying to do pagination SwiftUI

Progress View doesn't show on second + load when trying to do pagination. When I scroll to the bottom the progress view will appear once. But all the other times it doesn't. This only seems to occur when im using some sort of animation.
If I just have a static text like "Loading..." it works as expected. I added the section group where it checks a condition to verify if it should be presented or not. Not sure if I'm supposed to use something like "stop animating" like the loading indicator has in UIKit
struct ContentView: View {
#State var movies: [Movie] = []
#State var currentPage = 1
#State private var isLoading = false
var body: some View {
NavigationView {
List {
Section {
} header: {
Text("Top Movies")
}
ForEach(movies) { movie in
HStack(spacing: 8) {
AsyncImage(url: movie.posterURL, scale: 5) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
.cornerRadius(10)
} placeholder: {
ProgressView()
.frame(width: 100)
}
VStack(alignment: .leading, spacing: 10) {
Text(movie.title)
.font(.headline)
Text(movie.overview)
.lineLimit(5)
.font(.subheadline)
Spacer()
}
.padding(.top, 10)
}
.onAppear {
Task {
//Implementing infinite scroll
if movie == movies.last {
isLoading = true
currentPage += 1
movies += await loadMovies(page: currentPage)
isLoading = false
}
}
}
}
Section {
} footer: {
if isLoading {
HStack {
Spacer()
ProgressView()
.tint(.green)
Spacer()
}
} else {
EmptyView()
}
}
}
.navigationTitle("Movies")
.navigationBarTitleDisplayMode(.inline)
.listStyle(.grouped)
.task {
movies = await loadMovies()
}
.refreshable {
movies = await loadMovies()
}
}
}
}
even when I look at the view hierarchy its like the progress view square is there but the icon / loading indicator isn't showing:
If I add the overlay modifier it works but I don't like doing this because when I scroll back up before the content finishes loading the spinner is above the list view:?
.overlay(alignment: .bottom, content: {
if isLoading {
HStack {
Spacer()
ProgressView()
.tint(.green)
Spacer()
}
} else {
EmptyView()
}
})
We also had this problem, we think it is a bug of ProgressView. Our temporary correction is to identify the ProgressView with a unique id in order to render it again.
struct ContentView: View {
#State var id = 0
var body: some View {
ProgressView()
.id(id)
.onAppear {
...
id += 1
}
}
}

Replace images with thumbnail on failed - swiftUI

I am fetching images from Firebase storage, if there is no image on firebase, I want to show thumbnail,
Thats where I get the error
if let error = error {
Swift.print(error)
}
Here is my thumbnail
Image("shoePlaceHolder")
.resizable()
.aspectRatio(contentMode: .fit)
Here is the complete code
func getFullImageURL() {
let storage = Storage.storage()
let storagePath = "gs://on-switch.appspot.com/main/\(season)/"
let storageRef = storage.reference(forURL: storagePath)
let formattedImageURL = imageURL.absoluteString.replacingOccurrences(of: "file:///", with: "")
let ref = storageRef.child(formattedImageURL)
ref.downloadURL { url, error in
DispatchQueue.main.async {
if let error = error {
Swift.print(error)
} else if let url = url {
fullImageURL = url
} else {
Swift.print("No url and no error")
}
}
}
}
#ViewBuilder
var content: some View {
VStack {
VStack {
headerView()
HStack {
AsyncImage(url: $fullImageURL.wrappedValue) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
Image("shoePlaceHolder")
.resizable()
.aspectRatio(contentMode: .fit)
}.frame(width: 260, height: 180)
.onAppear(perform: getFullImageURL)
VStack(alignment: .leading) {
titleView()
subtitleView()
}
Spacer()
}
}
.padding()
OnDivider()
.padding(.horizontal)
SectionView(leadingView: {
Text("\(variants.count) variants")
.secondaryText()
}, trailingView: {
Image(systemName: "rectangle.expand.vertical")
.foregroundColor(Color(.secondaryLabel))
}).padding()
}
You have to use this line to invoke the listener for fullImageURL
fullImageURL = nil
and the complete code will look like this:
func getFullImageURL() {
let storage = Storage.storage()
let storagePath = "gs://on-switch.appspot.com/main/\(season)/"
let storageRef = storage.reference(forURL: storagePath)
let formattedImageURL = imageURL.absoluteString.replacingOccurrences(of: "file:///", with: "")
let ref = storageRef.child(formattedImageURL)
ref.downloadURL { url, error in
DispatchQueue.main.async {
if let error = error {
Swift.print(error)
fullImageURL = nil
} else if let url = url {
fullImageURL = url
} else {
Swift.print("No url and no error")
fullImageURL = nil
}
}
}
}
Since no image was shown as an error, you just need to focus on the error. Something like:
if error.localizedDescription == NoImageErrorString { // NoImageErrorString is something you can find in Firebase code
// show thumbnail here
} else {
// your original code
}
This works for me
struct ContentView: View {
#State private var url: URL?
func getFullImageURL() {
self.url = URL(string: "")
}
var body: some View {
VStack {
AsyncImage(url: url) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "star")
.resizable()
.aspectRatio(contentMode: .fit)
}
.frame(width: 260, height: 180)
.onAppear(perform: getFullImageURL)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to use a custom image, make sure shoePlaceHolder is added to the project Assets.
Image("shoePlaceHolder")
.
You can use a struct instead of a #State, #Published or whatever you're using to store fullImageURL.
Something like:
struct ImageWithErrorPlaceholder {
var fullImageURL: URL?
var showErrorPlaceholder: Bool = false
}
So you can simply inform the URL and the error as needed:
ref.downloadURL { url, error in
DispatchQueue.main.async {
if let error = error {
imageWithErrorPlaceholder.showErrorPlaceholder = true
Swift.print(error)
} else if let url = url {
imageWithErrorPlaceholder.fullImageURL = url
} else {
Swift.print("No url and no error")
}
}
}
That way you can display a different placeholder in case of error or no placeholder at all, depending or your needs:
AsyncImage(url: imageWithErrorPlaceholder.fullImageURL) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
if imageWithErrorPlaceholder.showErrorPlaceholder {
Image("shoePlaceHolder")
.resizable()
.aspectRatio(contentMode: .fit)
}
}

SwiftUI background image moves when keyboard shows

I have an image background, which should stay in place when the keyboard shows, but instead it moves up together with everything on the screen. I saw someone recommend using ignoresSafeArea(.keyboard), and this question Simple SwiftUI Background Image keeps moving when keyboard appears, but neither works for me. Here is my super simplified code sample. Please keep in mind that while the background should remain unchanged, the content itself should still avoid the keyboard as usual.
struct ProfileAbout: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("write something", text: $text)
Spacer()
Button("SomeButton") {}
}
.background(
Image("BackgroundName")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.keyboard)
)
}
}
Here a possible salvation:
import SwiftUI
struct ContentView: View {
#Environment(\.verticalSizeClass) var verticalSizeClass
#State var valueOfTextField: String = String()
var body: some View {
GeometryReader { proxy in
Image("Your Image name here").resizable().scaledToFill().ignoresSafeArea()
ZStack {
if verticalSizeClass == UserInterfaceSizeClass.regular { TextFieldSomeView.ignoresSafeArea(.keyboard) }
else { TextFieldSomeView }
VStack {
Spacer()
Button(action: { print("OK!") }, label: { Text("OK").padding(.horizontal, 80.0).padding(.vertical, 5.0).background(Color.yellow).cornerRadius(5.0) }).padding()
}
}
.position(x: proxy.size.width/2, y: proxy.size.height/2)
}
}
var TextFieldSomeView: some View {
return VStack {
Spacer()
TextField("write something", text: $valueOfTextField).padding(5.0).background(Color.yellow).cornerRadius(5.0).padding()
Spacer()
}
}
}
u can use GeometryReader
get parent View size
import SwiftUI
import Combine
struct KeyboardAdaptive: ViewModifier {
#State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.padding(.bottom, keyboardHeight)
.onReceive(Publishers.keyboardHeight) {
self.keyboardHeight = $0
}
}
}
}
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { $0.keyboardHeight }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}
struct ProfileAbout: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("write something", text: $text)
Spacer()
Button("SomeButton") {}
}
.background(
Image("BackgroundName")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.keyboard)
)
.keyboardAdaptive()
}
}

SwiftUI URLImage Horizontal ScrollView

So I've been going through a SwiftUI instagram tutorial and learnt how to load images uploaded by user to firebase in the standard 3x3 instagram view but am now wanting to expand my knowledge and practice doing it in horizontal scrollview.
Here's what I have to create grid view:
import SwiftUI
import URLImage
import FirebaseAuth
struct Photo: Identifiable {
let id = UUID()
var photo = ""
}
struct PhotoView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#EnvironmentObject var session: SessionStore
#ObservedObject var profileViewModel = ProfileViewModel()
var body: some View {
return
ScrollView {
if !profileViewModel.isLoading {
VStack(alignment: .leading, spacing: 1) {
// rows
ForEach(0..<self.profileViewModel.splitted.count) { index in
HStack(spacing: 1) {
// Columns
ForEach(self.profileViewModel.splitted[index], id: \.postId) { post in
URLImage(URL(string: post.mediaUrl)!,
content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
}).frame(width: UIScreen.main.bounds.width / 3, height: UIScreen.main.bounds.width / 3).clipped().cornerRadius(5)
}
}
}
}.frame(width: UIScreen.main.bounds.width, alignment: .leading).padding(.top, 2)
}
}.navigationBarTitle(Text("Photos"), displayMode: .inline).navigationBarBackButtonHidden(true).navigationBarItems(leading: Button(action : {
self.mode.wrappedValue.dismiss()
}) {
Image(systemName: "arrow.left")
}).onAppear {
self.profileViewModel.loadUserPosts(userId: Auth.auth().currentUser!.uid)
}
}
}
extension Array {
func splitted(into size:Int) -> [[Element]] {
var splittedArray = [[Element]]()
if self.count >= size {
for index in 0...self.count {
if index % size == 0 && index != 0 {
splittedArray.append(Array(self[(index - size)..<index]))
} else if (index == self.count) {
splittedArray.append(Array(self[index - 1..<index]))
}
}
} else {
splittedArray.append(Array(self[0..<self.count]))
}
return splittedArray
}
}
class ProfileViewModel: ObservableObject {
#Published var posts: [Post] = []
#Published var isLoading = false
var splitted: [[Post]] = []
func loadUserPosts(userId: String) {
isLoading = true
Api.User.loadPosts(userId: userId) { (posts) in
self.isLoading = false
self.posts = posts
self.splitted = self.posts.splitted(into: 3)
}
}
}
And this is what it looks like:
This is the sample code for what I am trying to achieve:
import SwiftUI
import URLImage
import FirebaseAuth
struct TestView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 2) {
ForEach(1..<5) { _ in
Image("photo3").resizable()
.clipShape(Rectangle())
.aspectRatio(contentMode: ContentMode.fill)
.frame(width: 100, height: 100).cornerRadius(10).opacity(1).shadow(radius: 4)
}
}
}.navigationBarTitle(Text("Photos"), displayMode: .inline).navigationBarBackButtonHidden(true).navigationBarItems(leading: Button(action : {
self.mode.wrappedValue.dismiss()
}) {
Image(systemName: "arrow.left")
})
Spacer()
}.padding()
}
}
and here is the sample image of what I want it to look like:
I'm really struggling to understand the ForLoop part and how I can retrieve the image to just be in a simple scrollView.
Any help would be much appreciated!
Thanks!
You want to loop over the posts in your model. Borrowing from your earlier code, you need something like this:
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 2) {
ForEach(self.profileViewModel.posts, id: \.postId) { post in
URLImage(URL(string: post.mediaUrl)!,
content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
}
)
.frame(width: 100, height: 100)
.clipped()
.cornerRadius(10)
.shadow(radius: 4)
}
}
}
vacawama has already posted the perfect solution to make it look like your example.
Just to add why you achieve the result, you are getting.
The difference between your code and the sample code is that you are using two ForEach, one for the rows and one for the columns. The array gets splitted with your extension, so you get rows and columns.
//Rows
ForEach(0..<self.profileViewModel.splitted.count) { index in
HStack(spacing: 1) {
// Columns
ForEach(self.profileViewModel.splitted[index], id: \.postId) { post in
Your comments already stating how it works. If you want to have all your images in a horizontal scroller, you just need one ForEach which outputs all your images in a ScrollView.
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 2) {
ForEach(self.profileViewModel.posts, id: \.postId) { post in

Issue using matchedGeometryEffect animation in ScrollView

Animating Views between a LazyVGrid and an HStack with another view in between them (in this case, a Button), using matchedGeometryEffect, works great:
Note how the animating views move above the Done button.
However, when the views are contained within a ScrollView, the animating views now move behind the intermediate view:
I've tried setting the zIndex of the ScrollViews to > 0 (or more) but this doesn't seem to change anything.
Any thoughts on how to fix this?
Person
struct Person: Identifiable, Equatable {
var id: String { name }
let name: String
var image: Image { Image(name) }
static var all: [Person] {
["Joe", "Kamala", "Donald", "Mike"].map(Person.init)
}
}
ContentView
struct ContentView: View {
#State var people: [Person]
#State private var selectedPeople: [Person] = []
#Namespace var namespace
var body: some View {
VStack(alignment: .leading, spacing: 0) {
ScrollView(.horizontal) {
SelectedPeopleView(people: $selectedPeople, namespace: namespace) { person in
withAnimation(.easeOut(duration: 1)) {
selectPerson(person)
}
}
.background(Color.orange)
}
doneButton()
ScrollView(.vertical) {
PeopleView(people: people, namespace: namespace) { person in
withAnimation(.easeOut(duration: 1)) {
deselectPerson(person)
}
}
}
Spacer()
}
.padding()
}
func selectPerson(_ person: Person) {
_ = selectedPeople.firstIndex(of: person).map { selectedPeople.remove(at: $0)}
people.append(person)
}
func deselectPerson(_ person: Person) {
_ = people.firstIndex(of: person).map { people.remove(at: $0)}
selectedPeople.append(person)
}
func doneButton() -> some View {
Button("Done") {
}
.font(.title2)
.accentColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color.gray)
}
}
SelectedPeopleView
struct SelectedPeopleView: View {
#Binding var people: [Person]
let namespace: Namespace.ID
let didSelect: (Person) -> Void
var body: some View {
HStack {
ForEach(people) { person in
Button(action: { didSelect(person) } ) {
Text(person.name)
.padding(10)
.background(Color.yellow.cornerRadius(6))
.foregroundColor(.black)
.matchedGeometryEffect(id: person.id, in: namespace)
}
}
}
.frame(height: 80)
}
}
PeopleView
struct PeopleView: View {
let people: [Person]
let namespace: Namespace.ID
let didSelect: (Person) -> Void
let columns: [GridItem] = Array(repeating: .init(.flexible(minimum: .leastNormalMagnitude, maximum: .greatestFiniteMagnitude)), count: 2)
var body: some View {
LazyVGrid(columns: columns) {
ForEach(people) { person in
Button(action: { didSelect(person) }) {
person.image
.resizable()
.scaledToFill()
.layoutPriority(-1)
.clipped()
.aspectRatio(1, contentMode: .fit)
.cornerRadius(6)
}
.zIndex(zIndex(for: person))
.matchedGeometryEffect(id: person.id, in: namespace)
}
}
}
func zIndex(for person: Person) -> Double {
Double(people.firstIndex(of: person)!)
}
}
This looks like a bug in SwiftUI, because even if you put Color.clear of any height in place of and instead of your doneButton (or even .padding of some height for bottom ScrollView) the effect will be the same.
As it is seen from view hierarchy there is nothing in between two ScrollView and rendering of images is performed in one single background view

Resources