I want to display a SwiftUI Image with a number. I might do so like this:
let number = 13
Image(systemName: "\(number).square.fill")
However, not every number has an SF Symbol — what if I wanted to display 134.square.fill but discovered that there isn't one, crashing the app?
How can I check if an SF Symbol exists without crashing the app?
The following code will show the question mark symbol, if one for the number does not exist. But you can adapt it to do anything.
struct ContentView: View {
var body: some View {
let number = 144
return VStack {
Image(uiImage: UIImage(systemName: "\(number).square.fill") ?? UIImage(systemName: "questionmark.square.fill")!)
}
}
}
After being in this situation myself, #kontiki’s answer really gave me a head start, but in his solution, the modifiers we apply to the image won’t work, so this is my approach:
We create a function that returns a String instead of an Image:
func safeSystemImage(_ image: String) -> String {
let image = "\(image).square.fill"
if UIImage(systemName: image) != nil {
return image
} else {
return "" // ← The image you want in case it doesn't exist.
}
}
Usage:
Image(systemName: safeSystemImage(yourImage))
This way we can change it’s color, size, etc…
Related
I am working on a CustomForEach which would act and work like a normal ForEach in SwiftUI, this CustomForEach has it own early days and it has some issues for use for me, which makes me to learn more about SwiftUI and challenge me to try to solve the issues, one of this issues is finding a way to destroy the unneeded Views instated of rendering all needed Views!
Currently when I update lowerBound the CustomForEach starts rendering for new range which is understandable. But the new range need less Views than before and that is not understandable to rendering them again for already rendered Views.
Goal: I want find a way to stop rendering all needed Views because they are already exist and there is no need to rendering again, and just removing the unneeded Views. And also I do not want start an another expensive calculation inside CustomForEach for finding out if the Views already exist!
struct TextView: View {
let string: String
var body: some View {
print("rendering " + string)
return HStack {
Text(string)
Circle().fill(Color.red).frame(width: 5, height: 5, alignment: .center)
}
}
}
struct CustomForEachView<Content: View>: View {
private let id: Int
let range: ClosedRange<Int>
let content: (Int) -> Content
init(range: ClosedRange<Int>, #ViewBuilder content: #escaping (Int) -> Content) {
self.id = range.lowerBound
self.range = range
self.content = content
}
// The issue is rendering all existed Views when lower Bound get updated, even we do not need to render new View in updating lower Bound!
var body: some View {
content(range.lowerBound)
if let suffixRange = suffix(of: range) {
CustomForEachView(range: suffixRange, content: content)
}
}
private func suffix(of range: ClosedRange<Int>) -> ClosedRange<Int>? {
return (range.count > 1) ? (range.lowerBound + 1)...range.upperBound : nil
}
}
struct ContentView: View {
#State private var lowerBound: Int = -2
#State private var upperBound: Int = 2
var body: some View {
HStack {
CustomForEachView(range: lowerBound...upperBound) { item in
TextView(string: item.description)
}
}
HStack {
Button("add lowerBound") { lowerBound += 1 }
Spacer()
Button("add upperBound") { upperBound += 1 }
}
.padding()
}
}
First of all, one thing important thing to understand is that a SwiftUI.View struct is not a view instance that is rendered on the screen. It's merely a description of the desired view hierarchy. The SwiftUI.View instances are going to be recreated and torn down a lot by the framework anyway.
The SwiftUI framework takes care of the actual rendering. It might use UIViews for this, or it might not. That's an implementation detail you shouldn't need to worry about in most cases.
That said, you might be able to help the framework by adding explicit ids to the views by using the id modifier. That way SwiftUI can use that to keep track of which view is which.
But, I'm not sure if that would actually help. Just an idea.
I am grabbing a screenshot of a sub-view in my SwiftUI View to immediately pass to a share sheet in order to share the image.
The view is of a set of questions from a text array rendered as a stack of cards. I am trying to get a screenshot of the question and make it share-able along with a link to the app (testing with a link to angry birds).
I have been able to capture the screenshot using basically Asperi's answer to the below question:
How do I render a SwiftUI View that is not at the root hierarchy as a UIImage?
My share sheet launches, and I've been able to use the "Copy" feature to copy the image, so I know it's actually getting a screenshot, but whenever I click "Message" to send it to someone, or if I just leave the share sheet open, the app crashes.
The message says it's a memory issue, but doesn't give much description of the problem. Is there a good way to troubleshoot this sort of thing? I assume it must be something with how the screenshot is being saved in this case.
Here are my extensions of View and UIView to render the image:
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
controller.view.backgroundColor = .clear
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
Here's an abbreviated version of my view - the button is about halfway down, and should call the private function at the bottom that renders the image from the View/UIView extensions, and sets the "questionScreenShot" variable to the rendered image, which is then presented in the share sheet.
struct TopicPage: View {
var currentTopic: Topic
#State private var currentQuestions: [String]
#State private var showShareSheet = false
#State var questionScreenShot: UIImage? = nil
var body: some View {
GeometryReader { geometry in
Button(action: {
self.questionScreenShot = render()
if self.questionScreenShot != nil {
self.showShareSheet = true
} else {
print("Did not set screenshot")
}
}) {
Text("Share Question").bold()
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [questionScreenShot!])
}
}
}
private func render() -> UIImage {
QuestionBox(currentQuestion: self.currentQuestions[0]).asImage()
}
}
I've found a solution that seems to be working here. I start the variable where the questionScreenShot gets stored as nil to start:
#State var questionScreenShot: UIImage? = nil
Then I just make sure to set it to 'render' when the view appears, which means it loads the UIImage so if the user clicks "Share Question" it will be ready to be loaded (I think there was an issue earlier where the UIImage wasn't getting loaded in time once the sharing was done).
It also sets that variable back to nil on disappear.
.onAppear {
self.currentQuestions = currentTopic.questions.shuffled()
self.featuredQuestion = currentQuestions.last!
self.questionScreenShot = render()
}
.onDisappear {
self.questionScreenShot = nil
self.featuredQuestion = nil
}
I have a bit of code in my app that generates a QR Code and scales it up (code reference I used from this link from Hackng with Swift. Now, I'm using the share sheet to allow the user to save the qr code to their camera roll and, it is working, but saving the image low res, and it saves to the camera roll blurry (and i assume if it is shared via other methods it will also be blurry)
Here is the code of my share sheet function:
struct ActivityView: UIViewControllerRepresentable {
let activityItems: [Any]
let applicationActivities: [UIActivity]?
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
return UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {
}
}
and here's the code in my view struct:
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [self.qrCodeImage])
}
Is there a trick to remove the interpolation on the image when it saves to the share sheet like the .interpolation(.none) on the image view itself?
Your problem is that the QR code image is actually tiny! Like really tiny:
Printing description of image:
<UIImage:0x60000202cc60 anonymous {23, 23}>
When you share this image, the way it will be displayed is dependant on the program or app that will display it, and is out of control of your app as far as I know.
However,
there is a way that you could potentially make it "pretty" in other apps, and this would be to increase the resolution to a larger amount so that when it's rendered it'll appear to have "sharp" pixels.
How would this be accomplished? I think I have an example buried somewhere in old code, I'll dig into it and see if I can find you an example ;)
Edit
I found the code:
extension UIImage {
func resized(toWidth width: CGFloat) -> UIImage? {
let canvasSize = CGSize(width: round(width), height: CGFloat(ceil(width/size.width * size.height)))
UIGraphicsBeginImageContextWithOptions(canvasSize, false, scale)
defer { UIGraphicsEndImageContext() }
let context = UIGraphicsGetCurrentContext();
context?.interpolationQuality = .none
// Set the quality level to use when rescaling
draw(in: CGRect(origin: .zero, size: canvasSize))
let r = UIGraphicsGetImageFromCurrentImageContext()
return r
}
}
The trick is to provide a way to scale the image, but the real magic is on line 7:
context?.interpolationQuality = .none
If you exclude this line, you'll get blurry images, which is what the OS does by default because you don't generally want to see the pixel edges in images.
You could use this extension like so:
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [self.qrCodeImage.resized(toWidth: 512) ?? UIImage()])
}
However, this may be resizing the image way more often than necessary. Optimally you'd resize it in the same function that you generate it.
I'm trying to create a 4x4 grid of images, and I'd like it to scale from 1 image up to 4.
This code works when the images provided come from a regular array
var images = ["imageOne", "imageTwo", "imageThree", "imageFour"]
However it does not work if the array comes from an object we are bound to:
#ObjectBinding var images = ImageLoader() //Where our array is in images.images
My initialiser looks like this:
init(imageUrls urls: [URL]){
self.images = ImageLoader(urls)
}
And my ImageLoader class looks like this:
class ImageLoader: BindableObject {
var didChange = PassthroughSubject<ImageLoader, Never>()
var images = [UIImage]() {
didSet{
DispatchQueue.main.async {
self.didChange.send(self)
}
}
}
init(){
}
init(_ urls: [URL]){
for image in urls{
//Download image and append to images array
}
}
}
The problem arises in my View
var body: some View {
return VStack {
if images.images.count == 1{
Image(images.images[0])
.resizable()
} else {
Text("More than one image")
}
}
}
Upon compiling, I get the error generic parameter 'FalseContent' could not be inferred, where FalseContent is part of the SwiftUI buildEither(first:) function.
Again, if images, instead of being a binding to ImageLoader, is a regular array of Strings, it works fine.
I'm not sure what is causing the issue, it seems to be caused by the binding, but I'm not sure how else to do this.
The problem is your Image initialiser, your passing a UIImage, so you should call it like this:
Image(uiImage: images.images[0])
Note that when dealing with views, flow control is a little complicated and error messages can be misleading. By commenting the "else" part of the IF statement of your view, the compiler would have shown you the real reason why it was failing.
In my app I'm allowing the user the change some of the UI elements based on color. So, I have three versions of an image which can be used for a button and I'd like to select the image programmatically:
PSEUDO CODE
"image0", "image1", "image3"
var userChoice:integer
myButton.setImage("myImage"+userChoice , .normal)
I've seen this solution in SO:
Programmatically access image assets
What would be the Swift equivalent code?
Right now I'm using image literal:
self.But_Settings.setImage(#imageLiteral(resourceName: "settingswhite"), for: UIControlState.normal)
but of course Xcode changes this part "#imageLiteral(resourceName: "settingswhite")" to an icon which cannot be edited.
Then don't use image literal. Image literals are just that - a hard value in code that you can't change during run time. Load an image dynamically from your bundle:
if let image = UIImage(named: "myImage" + userChoice) {
self.But_Settings.setImage(image, for: .normal)
}
Do you think this would help? : But_Settings.setImage(UIImage(named: "play.png"), for: UIControlState.normal). Here you are using name of the asset
Since it's a limited number of choices it sounds like a good place for an enum.
enum ImageChoice: Int {
case zero = 0, one, two
var image: UIImage {
switch self {
case .zero:
return // Some Image, can use the icon image xcode provides now
case .one:
return //another image
case .two:
return //another image
}
}
}
Then you can easily get the correct image by initializing the enum by using an Int value.
guard let userChoice = ImageChoice(rawValue: someInt) else { return //Default Image }
let image = userChoice.image