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.
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 have a problem that's cropped up since I've updated my Xcode... Unless the user hits pause in the audio view. My audio player continues to play when the user changes views however I would like the audio to stop when the user exits the player view (ItemDetail) (for example when the user goes to Content View)
Previously I was using the below at the start of the Content View and that had worked but now it doesn't:
init() { sounds.pauseSounds() }
I've also tried this (which hasn't worked either):
}// end of navigation view .onAppear{sounds.pauseSounds()}
This is my sounds class:
class Sounds:ObservableObject {
var player:AVAudioPlayer?
// let shared = Sounds()
func playSounds(soundfile: String) {
if let path = Bundle.main.path(forResource: soundfile, ofType: nil){
do{
player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
player?.prepareToPlay()
player?.play()
}catch {
print("Error can't find sound file or something's not working with the sounds model")
}
}
}
// This method used to work at pausing the sound (pre-update)
func pauseSounds() {
player?.pause()
}
// over rides the sound defaulting to bluetooth speaker only and sends it to phone speaker if bluetooth is not available (headset/speaker) however if it is available it'll play through that.
func overRideAudio() {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSession.Category.playback, mode: .spokenAudio, options: .defaultToSpeaker)
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("error.")
}
}
}
And this is the player view:
struct ItemDetail: View {
#State var isPlaying = false
var item : SessionsItem
#ObservedObject var soundsPlayer = Sounds()
var body: some View {
HStack {
Button(action: {
if self.isPlaying{
// Sounds.player?.pause()
self.isPlaying = false
soundsPlayer.pauseSounds()
}
else{
self.isPlaying = true
soundsPlayer.playSounds(soundfile: "\(self.item.name).mp3")
soundsPlayer.overRideAudio()
}
You are creating another Sound instance so you do not have access to the open audiio instance. You can make Sounds class as single instance or pass the Sounds instance to the detail. Dont create a new one.
onAppear is not a reliable way to do work like this SwiftUI does a lot of preloading (It is very apparent with a List). It depends on the device and its capabilities. Sometimes screens "appear" when they are preloaded not necessarily when the user is looking at it.
With the List example below you can see it in simulator.
On iPhone 8 I get 12 onAppear print statements in the console when I initially load it and on iPhone 12 Pro Max I get 17 print statements.
If the cells are more/less complex you get more/less statements accordingly. You should find another way to do this.
import SwiftUI
struct ListCellAppear: View {
var idx: Int
var body: some View {
HStack{
Text(idx.description)
Image(systemName: "checkmark")
}.onAppear(){
print(idx.description)
}
}
}
struct ListAppear: View {
var body: some View {
List(0..<1000){ idx in
ListCellAppear(idx: idx).frame(height: 300)
}
}
}
struct ListAppear_Previews: PreviewProvider {
static var previews: some View {
ListAppear()
}
}
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'm trying to update my MVVM-Coordinators pattern to use it with SwiftUI and Combine.
To preserve abstraction, I use a ScenesFactory that handle the creation of, well, my scenes like the following:
final class ScenesFactory {
let viewModelsFactory = SceneViewModelsFactory()
}
extension ScenesFactory: SomeFlowScenesFactory {
func makeSomeScene() -> Scene {
let someSceneInput = SomeSceneInput()
let someSceneViewModel = viewModelsFactory.makeSomeSceneViewModel(with: someSceneInput)
let someSceneView = SomeSceneView()
someSceneView.viewModel = someSceneViewModel
return BaseScene(view: someSceneView, viewModel: someSceneViewModel)
}
}
Here is the implementation of a my Scene protocol:
public protocol Scene {
var view: some View { get }
var viewModel: ViewModelOutput { get }
init(view: some View, viewModel: ViewModelOutput)
}
The goal here is to be able to use UIHostingControllerto present my someScene.view but the compiler throws an error at my Scene protocol:
I thought the point of the some keyword was precisely to use generic protocols as a return type.
What am I missing ?
I thought the point of the some keyword was precisely to use generic protocols as a return type.
Yes, but it seems that it's doesn't work that way in a protocol declaration, not really sure why.
But there is a way to fix this, use an associatedtype that is constrained to View, and the compiler stop complaining.
public protocol Scene {
associatedtype Content: View
var view: Content { get }
}
struct V: Scene {
var view: some View {
EmptyView()
}
}
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…