Replace images with thumbnail on failed - swiftUI - xcode

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)
}
}

Related

What is this weird overlay shown on my SettingsPane?

Here's my code, I don't get why this results in this transparent overlay when I move focus to the Text or SecureField components
struct SettingsPane: View {
// MARK: View state
#State private var isSecured: Bool = true
#State private var showApiKeyPopover: Bool = false
// MARK: Preference Storage
#AppStorage("preference_showWorkboard") var showWorkboard = true
#AppStorage("preference_userName") var userName = ""
#AppStorage("preference_jiraApiKey") var jiraApiKey = ""
// MARK: View dimensions
var frameWidth:CGFloat = 200
var body: some View {
Form {
Section(header: Text("Credentials")) {
TextField("User", text: $userName)
.textContentType(.username)
.frame(width: frameWidth)
HStack(alignment: .center) {
Group {
if isSecured {
SecureField("Jira API Key", text: $jiraApiKey)
.textContentType(.password)
.frame(width: frameWidth)
.lineLimit(1)
} else {
TextField("Jira API Key", text: $jiraApiKey)
.textContentType(.password)
.frame(width: frameWidth)
.lineLimit(1)
}
}.popover(isPresented: $showApiKeyPopover) {
Text("To get an API key visit your Jira profile.\nClick 'Personal Access Tokens' and create one named 'Firehose' and add it to this field")
.padding()
}
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.accentColor(.gray)
}
.buttonStyle(.borderless)
Button(action: {
showApiKeyPopover.toggle()
}) {
Image(systemName: "questionmark.circle.fill")
.accentColor(.gray)
}
.buttonStyle(.borderless)
}
Divider()
Section(header: Text("Features")) {
Toggle("Show Workboard", isOn: $showWorkboard)
}
}
}
.padding()
.frame(minWidth: 400, maxWidth: 400)
}
}
It works fine with me (macOS 13.0, Xcode 14.1). I get this ...
Could it be some password utility interfering?

SwiftUI, CloudKit and Images

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
}
}
}
}

Show a sheet in response to a drop

I'm implementing drag and drop, and have a case where I need the user to decide what to do in response to a drop. So I want to bring up a sheet to ask the user for input. The problem is that the sheet doesn't appear until I drag another item to the same view. This does make sense, so I'm looking for a way to handle this differently.
The current approach looks like this (simplified):
struct SymbolInfo {
enum SymbolType {
case string, systemName
}
var type: SymbolType
var string: String
}
struct MyView: View, DropDelegate {
#State var sheetPresented = false
#State var droppedText = ""
static let dropTypes = [UTType.utf8PlainText]
var textColor = NSColor.white
private var frameRect: CGRect = .null
private var contentPath: Path = Path()
private var textRect: CGRect = .null
#State private var displayOutput: SymbolInfo
#State private var editPopoverIsPresented = false
// There's an init to set up the display output, the various rects and path
var body: some View {
ZStack(alignment: stackAlignment) {
BackgroundView() // Draws an appropriate background
.frame(width: frameRect.width, height: frameRect.height)
if displayOutput.type == .string {
Text(displayOutput.string)
.frame(width: textRect.width, height: textRect.height, alignment: .center)
.foregroundColor(textColour)
.font(displayFont)
.allowsTightening(true)
.lineLimit(2)
.minimumScaleFactor(0.5)
}
else {
Image(systemName: displayOutput.string)
.frame(width: textRect.width, height: textRect.height, alignment: .center)
.foregroundColor(textColour)
.minimumScaleFactor(0.5)
}
}
.onAppear {
// Retrieve state information from the environment
}
.focusable(false)
.allowsHitTesting(true)
.contentShape(contentPath)
.onHover { entered in
// Populates an inspector
}
.onTapGesture(count: 2) {
// Handle a double click
}
.onTapGesture(count: 1) {
// Handle a single click
}
.popover(isPresented: $editPopoverIsPresented) {
// Handles a popover for editing data
}
.onDrop(of: dropTypes, delegate: self)
.sheet(sheetPresented: $sheetPresented, onDismiss: sheetReturn) {
// sheet to ask for the user's input
}
}
func sheetReturn() {
// act on the user's input
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: dropTypes).first {
item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { (textData, error) in
if let textData = String(data: textData as! Data, encoding: .utf8) {
if (my condition) {
sheetIsPresented = true
droppedText = textData
}
else {
// handle regular drop
}
}
}
return true
}
return false
}
}
So my reasoning is that the drop sets sheetPresented to true, but then it doesn't get acted on until the view is rebuilt, such as on dragging something else to it. But I'm still new to SwiftUI, so I may be incorrect.
Is there a way to handle this kind of interaction that I haven't found?
I never was able to exactly reproduce the problem, but the issue related to trying to have more than one kind of sheet that could be shown, depending on conditions. The solution was to break up the original view into a family of views that encapsulated the different behaviours, and show the appropriate one rather than try to make one view do everything.
I won't show the whole code, since it's too deeply embedded in the app, but here's a demo app that works correctly:
import SwiftUI
import UniformTypeIdentifiers
#main
struct DragAndDropSheetApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
HStack() {
TargetView(viewType: .normal, viewText: "A")
.frame(width: 40, height: 40, alignment: .top)
TargetView(viewType: .protected, viewText: "B")
.frame(width: 40, height: 40, alignment: .top)
TargetView(viewType: .normal, viewText: "C")
.frame(width: 40, height: 40, alignment: .top)
TargetView(viewType: .protected, viewText: "D")
.frame(width: 40, height: 40, alignment: .top)
}
.padding()
}
}
enum ViewType {
case normal, protected
}
struct TargetView: View, DropDelegate {
#State private var sheetPresented = false
#State var viewType: ViewType
#State var viewText: String
#State private var dropText = ""
#State private var dropType: DropActions = .none
static let dropTypes = [UTType.utf8PlainText]
var body: some View {
ZStack(alignment: .center) {
Rectangle()
.foregroundColor(viewType == .normal ? .blue : .red)
Text(viewText)
.foregroundColor(.white)
.frame(width: nil, height: nil, alignment: .center)
}
.focusable(false)
.allowsHitTesting(true)
.onDrop(of: TargetView.dropTypes, delegate: self)
.sheet(isPresented: $sheetPresented, onDismiss: handleSheetReturn) {
ProtectedDrop(isPresented: $sheetPresented, action: $dropType)
}
}
func handleSheetReturn() {
switch dropType {
case .append:
viewText += dropText
case .replace:
viewText = dropText
case .none:
// Nothing to do
return
}
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: TargetView.dropTypes).first {
item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { textData, error in
if let textData = String(data: textData as! Data, encoding: .utf8) {
if viewType == .normal {
viewText = textData
}
else {
dropText = textData
sheetPresented = true
}
}
}
return true
}
return false
}
}
enum DropActions: Hashable {
case append, replace, none
}
struct ProtectedDrop: View {
#Binding var isPresented: Bool
#Binding var action: DropActions
var body: some View {
VStack() {
Text("This view is protected. What do you want to do?")
Picker("", selection: $action) {
Text("Append the dropped text")
.tag(DropActions.append)
Text("Replace the text")
.tag(DropActions.replace)
}
.pickerStyle(.radioGroup)
HStack() {
Spacer()
Button("Cancel") {
action = .none
isPresented.toggle()
}
.keyboardShortcut(.cancelAction)
Button("OK") {
isPresented.toggle()
}
.keyboardShortcut(.defaultAction)
}
}
.padding()
}
}

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 macOS sheet does not dismiss sometimes

I have implemented a sheet to edit the values of a client.
It's normally possible to edit the client and close the sheet after pressing the OK-Button. But if the sheet is open for a longer time it is not possible to dismiss the sheet. Nothing happens and they only way to proceed is to quit the program.
Does anyone have an idea why this happens sometimes?
struct ContentView: View {
#State private var showingEditClient = false
var body: some View {
VStack{
HStack {
Button(action: showEditClientSheet) {
Text("Edit Client")
}
.sheet(isPresented: $showingEditClient) {
EditClientSheet()
}
}
}
.frame(minWidth: 400, minHeight: 400)
}
func showEditClientSheet(){
showingEditClient.toggle()
}
}
struct EditClientSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var name = "Max"
var body: some View {
VStack {
Form {
TextField("Name", text: $name)
}
HStack{
Button(action: cancel) {
Text("Abbrechen")
}
Button(action: editClient) {
Text("Ok")
}
}
}
.frame(minWidth: 200, minHeight: 200)
}
func editClient() {
NSApp.keyWindow?.makeFirstResponder(nil)
//Check if content is correct to save
if name != "" {
//store the changes
self.presentationMode.wrappedValue.dismiss()
}else {
//show Alert
}
}
func cancel() {
self.presentationMode.wrappedValue.dismiss()
}
}

Resources