get the right index onTapGesture with AsyncImage - swiftui

While iterating over images and loading AsyncImages, the .onTapGesture does not refer to the clicked element.
Is this due to View refresh on image loading? How to bypass this issue?
var images: [String] = [
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_brousset.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_sommet-ts-crete.jpg",
"https://www.trinum.com/ibox/ftpcam/mega_mtgenevre_sommet-des-gondrans.jpg"
]
struct thumbnail: View {
#State var mainImageUrl: String = images[0];
var body: some View {
VStack {
AsyncImage(url: URL(string: mainImageUrl)) { image in
image
.resizable().scaledToFit().frame(height: 350)
} placeholder: {
ProgressView()
}.frame(height: 350).cornerRadius(10)
HStack {
ForEach(images, id: \.self) { imageUrl in
AsyncImage(url: URL(string: imageUrl)) { sourceImage in
sourceImage
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100, alignment: .center)
.clipped()
} placeholder: {
ProgressView()
}.onTapGesture {
self.mainImageUrl = imageUrl
}
}
}
}
}
}

It sort of works if you just flip the frame and aspectRatio as in the code below.
However it is very slow and you are constantly re-downloading the images whenever you click on a thumbnail.
The last image is specially slow.
struct ContentView: View {
var body: some View {
Thumbnail()
}
}
struct Thumbnail: View {
var images: [String] = [
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_brousset.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_sommet-ts-crete.jpg",
"https://www.trinum.com/ibox/ftpcam/mega_mtgenevre_sommet-des-gondrans.jpg"
]
#State var mainImageUrl: String = "https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg"
var body: some View {
VStack {
AsyncImage(url: URL(string: mainImageUrl)) { image in
image.resizable().scaledToFit().frame(height: 350)
} placeholder: {
ProgressView()
}.frame(height: 350).cornerRadius(10)
HStack {
ForEach(images, id: \.self) { imageUrl in
AsyncImage(url: URL(string: imageUrl)) { sourceImage in
sourceImage
.resizable()
.frame(width: 100, height: 100) // <--- here
.aspectRatio(contentMode: .fill) // <--- here
.clipped()
} placeholder: {
ProgressView()
}.onTapGesture {
self.mainImageUrl = imageUrl
}
}
}
}
}
}
IMHO, a better way is to use a different approach to avoid the constant downloading of images.
You could download the pictures in parallel only once, using swift async/await concurrency.
Such as in this code:
struct Thumbnail: View {
#StateObject var loader = ImageLoader()
#State var selectedPhoto: PhotoImg?
var body: some View {
VStack {
if loader.images.count < 1 {
ProgressView()
} else {
Image(uiImage: selectedPhoto?.image ?? UIImage(systemName: "smiley")!)
.frame(height: 350).cornerRadius(10)
ScrollView {
HStack (spacing: 10) {
ForEach(loader.images) { photo in
Image(uiImage: photo.image)
.resizable()
.frame(width: 100, height: 100)
.aspectRatio(contentMode: .fill)
.onTapGesture {
selectedPhoto = photo
}
}
}
}
.onAppear {
if let first = loader.images.first {
selectedPhoto = first
}
}
}
}
.task {
await loader.loadParallel()
}
}
}
class ImageLoader: ObservableObject {
#Published var images: [PhotoImg] = []
let urls: [String] = [
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_brousset.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_sommet-ts-crete.jpg",
"https://www.trinum.com/ibox/ftpcam/mega_mtgenevre_sommet-des-gondrans.jpg"
]
func loadParallel() async {
return await withTaskGroup(of: (String, UIImage).self) { group in
for str in urls {
if let url = URL(string: str) {
group.addTask { await (url.absoluteString, self.loadImage(url: url)) }
}
}
for await result in group {
DispatchQueue.main.async {
self.images.append(PhotoImg(url: result.0, image: result.1))
}
}
}
}
private func loadImage(url: URL) async -> UIImage {
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let img = UIImage(data: data) { return img }
}
catch { print(error) }
return UIImage()
}
}
struct PhotoImg: Identifiable, Hashable {
let id = UUID()
var url: String
var image: UIImage
}

This was very useful. Thanks. I simply had to lines flipped:
.scaledToFill()
.frame(width: 80, height: 80)
Yet it seemed those two lines in the order presented above caused this to work properly. Any idea why this is the case?

Related

The compiler is unable to type-check this expression in reasonable time, My view is not too big though

The following file fails every single time and it won't even let me see a preview of the view. I have know idea what is going on. I have quit Xcode, I have cleaned the build folder. Everything I can think of, I have done. I am working on Xcode version 12.2. Anybody have any ideas?
struct weatherToggle : Identifiable {
let id = UUID()
var value : Bool = false
}
struct Sliders : Identifiable {
var id: UUID
var percent: Double
var name: String
init( percent: Double, name: String ) {
self.id = UUID()
self.percent = percent
self.name = name
}
}
struct MyNodeView : View {
#Binding var myNode : Sliders
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.percent))%").font(.footnote)
Slider(value: $myNode.percent, in: 0 ... 100).padding()
}
}
}
struct OperatingConditionsView: View {
#State var selection: Int? = nil
let lightGray = Color(hue: 1.0, saturation: 0.0, brightness: 0.392)
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var defensiveLayers = [Sliders]()
#State var sensorsAndSupports = [Sliders]()
// #Binding public var threats: [Any]
#State var availableLaunchPlatformSelections: [String] = []
#State var items = [weatherToggle(),
weatherToggle(),
weatherToggle()]
#State public var battery = "South Korea"
#State var atmosphericsSeverity = [String]()
#State var heliosphericsSeverity = [String]()
var conditions = ["light", "moderate", "severe"]
#State var heliospherics = [String]()
#State var atmospherics = [String]()
//#State var defensiveLayers = [String]()
#State var availableLaunchPlatforms = [String]()
//#State var sensorsAndSupport = [String]()
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("OPERATING CONDITIONS").fontWeight(.bold)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
Button("load stored attack parameter set"){
}.padding(.leading, 30)
Text("ASSET READINESS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
Group {
Text("Available launch platforms").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
VStack {
List {
ForEach(availableLaunchPlatforms, id: \.self) { launchPlatform in
MultipleSelectionRow(title: launchPlatform, isSelected: self.availableLaunchPlatformSelections.contains(launchPlatform)) {
if self.availableLaunchPlatformSelections.contains(launchPlatform) {
self.availableLaunchPlatformSelections.removeAll(where: { $0 == launchPlatform })
}
else {
self.availableLaunchPlatformSelections.append(launchPlatform)
}
}.font(.custom("Gill Sans", size: 12)).foregroundColor(.gray)
}
}.frame(height: 250).font(.footnote)
}
}
Group {
Text("Other defensive layers").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
HStack {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray).frame(maxHeight: .infinity)
}
}
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
MyNodeView(myNode: $defensiveLayers[i])
}
}
}.padding(.horizontal, 30)
}
Group {
Text("Sensors & Support").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
HStack {
VStack {
ForEach(sensorsAndSupports.indices, id: \.self) { i in
Text(sensorsAndSupports[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray).frame(maxHeight: .infinity)
}
}
VStack {
ForEach(sensorsAndSupports.indices, id: \.self) { i in
MyNodeView(myNode: $sensorsAndSupports[i])
}
}
}.padding(.horizontal, 30)
}
Group {
Text("ATMOSPHERICS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
ForEach(0 ..< atmospherics.count, id: \.self) { i in
HStack {
Menu {
ForEach(0 ..< conditions.count) { j in
Button(conditions[j]) {
atmosphericsSeverity[i] = conditions[j]
}
}
} label: {
Text(atmospherics[i])
Image(systemName: "cloud.drizzle")
}.frame(maxWidth: .infinity)
Text("Current: " + atmosphericsSeverity[i]).frame(maxWidth: .infinity).font(.footnote).foregroundColor(.gray)
}.padding()
.foregroundColor(.white).background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.padding()
}
}
Group {
Text("HELIOSPHERICS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
ForEach(0 ..< heliospherics.count, id: \.self) { i in
HStack {
Menu {
ForEach(0 ..< conditions.count) { j in
Button(conditions[j]) {
heliosphericsSeverity[i] = conditions[j]
}
}
} label: {
Text(heliospherics[i])
Image(systemName: "cloud.drizzle")
}.frame(maxWidth: .infinity)
Text("Current: " + heliosphericsSeverity[i]).frame(maxWidth: .infinity).font(.footnote).foregroundColor(.gray)
}.padding()
.foregroundColor(.white).background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.padding()
}
}
HStack {
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}.frame(maxWidth: .infinity)
Button("Run"){
let jsonObj: Any = ["Threats": [], "launchPlatforms": availableLaunchPlatformSelections, "defensiveLayers": defensiveLayers.map({ ["layer": $0.name, "percentage": Int($0.percent) ] }), "sensorsAndSupports": sensorsAndSupports.map({ ["SensorSupport": $0.name, "percentage": Int($0.percent) ] }), "atmospherics:": atmosphericsSeverity.map({ ["weather": "", "intensity": $0 ] })]
print(convertJSON(array: jsonObj))
}.foregroundColor(.red).frame(maxWidth: .infinity)
}.frame(maxWidth: .infinity).padding(.all, 30)
}
}.onAppear() {
loadOpConditions(country: battery.replacingOccurrences(of: " ", with: ""), completionHandler: { (data: [Dictionary<String, Any>]) in
for row in data {
for _ in row["atmosperics"] as! [String]
{
atmosphericsSeverity.append("light")
}
for i in row["heliospherics"] as! [String]
{
heliosphericsSeverity.append("light")
}
heliospherics = row["heliospherics"] as! [String]
atmospherics = row["atmosperics"] as! [String]
for i in row["sensorsAndSupport"] as! [String]
{
sensorsAndSupports.append(Sliders(percent: 0, name: i))
}
for i in row["defensiveLayers"] as! [String]
{
defensiveLayers.append(Sliders(percent: 0, name: i ))
}
availableLaunchPlatforms = row["availableLaunchPlatforms"] as! [String]
}
})
}
}
func convertJSON(array: Any) -> String
{
do {
let jsonData = try JSONSerialization.data(withJSONObject: array, options: [])
if let jsonString = String(data: jsonData, encoding: String.Encoding.utf8) {
//print(jsonString)
return jsonString
}
else
{
return ""
}
} catch {
return "error"
}
}
func loadOpConditions(country: String, completionHandler: #escaping ([Dictionary<String, Any>]) -> Void) -> Void {
var request = URLRequest(url: URL(string: "https://salusdatalab.com/api/OperatingConditions/" + country)!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
// request.addValue("Bearer \(LoginViewController.myToken.bearerToken)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data
else {
//p rint(String(describing: error))
return
}
//p rint(String(data: data, encoding: .utf8)!)
guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
else { return }
guard let rootArray = json as? Array<Dictionary<String,Any>>
else { return }
// The outer/root array seems useless from what you have shown in your JSON above, so this is to get to the array of dictionaries.
completionHandler(rootArray)
}
task.resume()
}
}
struct OperatingConditionsView_Previews: PreviewProvider {
static var previews: some View {
OperatingConditionsView()
}
}
I am putting this in here because it says your post is mostly code. Thank you for your help in advance.
To debug this type of situation, you'll generally want to split your view/body section up into multiple chunks that you can comment out/modify.
By doing this, I narrowed it down to your fourth and fifth groups. Code first, then explanation:
struct OperatingConditionsView: View {
#State var selection: Int? = nil
let lightGray = Color(hue: 1.0, saturation: 0.0, brightness: 0.392)
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var defensiveLayers = [Sliders]()
#State var sensorsAndSupports = [Sliders]()
// #Binding public var threats: [Any]
#State var availableLaunchPlatformSelections: [String] = []
#State var items = [weatherToggle(),
weatherToggle(),
weatherToggle()]
#State public var battery = "South Korea"
#State var atmosphericsSeverity = [String]()
#State var heliosphericsSeverity = [String]()
var conditions = ["light", "moderate", "severe"]
#State var heliospherics = [String]()
#State var atmospherics = [String]()
//#State var defensiveLayers = [String]()
#State var availableLaunchPlatforms = [String]()
//#State var sensorsAndSupport = [String]()
var topSection: some View {
Group {
Text("OPERATING CONDITIONS").fontWeight(.bold)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
Button("load stored attack parameter set"){
}.padding(.leading, 30)
Text("ASSET READINESS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
}
}
var firstGroup : some View {
Group {
Text("Available launch platforms").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
VStack {
List {
ForEach(availableLaunchPlatforms, id: \.self) { launchPlatform in
// MultipleSelectionRow(title: launchPlatform, isSelected: self.availableLaunchPlatformSelections.contains(launchPlatform)) {
// if self.availableLaunchPlatformSelections.contains(launchPlatform) {
// self.availableLaunchPlatformSelections.removeAll(where: { $0 == launchPlatform })
// }
// else {
// self.availableLaunchPlatformSelections.append(launchPlatform)
// }
// }.font(.custom("Gill Sans", size: 12)).foregroundColor(.gray)
}
}.frame(height: 250).font(.footnote)
}
}
}
//
var secondGroup : some View {
Group {
Text("Other defensive layers").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
HStack {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray).frame(maxHeight: .infinity)
}
}
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
MyNodeView(myNode: $defensiveLayers[i])
}
}
}.padding(.horizontal, 30)
}
}
//
var thirdGroup : some View {
Group {
Text("Sensors & Support").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
HStack {
VStack {
ForEach(sensorsAndSupports.indices, id: \.self) { i in
Text(sensorsAndSupports[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray).frame(maxHeight: .infinity)
}
}
VStack {
ForEach(sensorsAndSupports.indices, id: \.self) { i in
MyNodeView(myNode: $sensorsAndSupports[i])
}
}
}.padding(.horizontal, 30)
}
}
//
var fourthGroup : some View {
Group {
Text("ATMOSPHERICS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
ForEach(0 ..< atmospherics.count, id: \.self) { (i:Int) in //<-- HERE
HStack {
Menu {
ForEach(0 ..< conditions.count) { (j:Int) in //<-- HERE
Button(conditions[j]) {
atmosphericsSeverity[i] = conditions[j]
}
}
} label: {
Text(atmospherics[i])
Image(systemName: "cloud.drizzle")
}
.frame(maxWidth: .infinity)
Text("Current: " + atmosphericsSeverity[i]).frame(maxWidth: .infinity).font(.footnote).foregroundColor(.gray)
}
.padding()
.foregroundColor(.white).background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.padding()
}
}
}
//
var fifthGroup : some View {
Group {
Text("HELIOSPHERICS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
ForEach(0 ..< heliospherics.count, id: \.self) { (i:Int) in //<-- HERE
HStack {
Menu {
ForEach(0 ..< conditions.count) { (j:Int) in //<-- HERE
Button(conditions[j]) {
heliosphericsSeverity[i] = conditions[j]
}
}
} label: {
Text(heliospherics[i])
Image(systemName: "cloud.drizzle")
}.frame(maxWidth: .infinity)
Text("Current: " + heliosphericsSeverity[i]).frame(maxWidth: .infinity).font(.footnote).foregroundColor(.gray)
}.padding()
.foregroundColor(.white).background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.padding()
}
}
}
var lastButtons : some View {
HStack {
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}.frame(maxWidth: .infinity)
Button("Run"){
let jsonObj: Any = ["Threats": [], "launchPlatforms": availableLaunchPlatformSelections, "defensiveLayers": defensiveLayers.map({ ["layer": $0.name, "percentage": Int($0.percent) ] }), "sensorsAndSupports": sensorsAndSupports.map({ ["SensorSupport": $0.name, "percentage": Int($0.percent) ] }), "atmospherics:": atmosphericsSeverity.map({ ["weather": "", "intensity": $0 ] })]
print(convertJSON(array: jsonObj))
}.foregroundColor(.red).frame(maxWidth: .infinity)
}.frame(maxWidth: .infinity).padding(.all, 30)
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
topSection
firstGroup
secondGroup
thirdGroup
fourthGroup
fifthGroup
lastButtons
}
}.onAppear() {
onAppearFunc()
}
}
func onAppearFunc() {
loadOpConditions(country: battery.replacingOccurrences(of: " ", with: ""), completionHandler: { (data: [Dictionary<String, Any>]) in
for row in data {
for _ in row["atmosperics"] as! [String]
{
atmosphericsSeverity.append("light")
}
for i in row["heliospherics"] as! [String]
{
heliosphericsSeverity.append("light")
}
heliospherics = row["heliospherics"] as! [String]
atmospherics = row["atmosperics"] as! [String]
for i in row["sensorsAndSupport"] as! [String]
{
sensorsAndSupports.append(Sliders(percent: 0, name: i))
}
for i in row["defensiveLayers"] as! [String]
{
defensiveLayers.append(Sliders(percent: 0, name: i ))
}
availableLaunchPlatforms = row["availableLaunchPlatforms"] as! [String]
}
})
}
func convertJSON(array: Any) -> String
{
do {
let jsonData = try JSONSerialization.data(withJSONObject: array, options: [])
if let jsonString = String(data: jsonData, encoding: String.Encoding.utf8) {
//print(jsonString)
return jsonString
}
else
{
return ""
}
} catch {
return "error"
}
}
func loadOpConditions(country: String, completionHandler: #escaping ([Dictionary<String, Any>]) -> Void) -> Void {
var request = URLRequest(url: URL(string: "https://salusdatalab.com/api/OperatingConditions/" + country)!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
// request.addValue("Bearer \(LoginViewController.myToken.bearerToken)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data
else {
//p rint(String(describing: error))
return
}
//p rint(String(data: data, encoding: .utf8)!)
guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
else { return }
guard let rootArray = json as? Array<Dictionary<String,Any>>
else { return }
// The outer/root array seems useless from what you have shown in your JSON above, so this is to get to the array of dictionaries.
completionHandler(rootArray)
}
task.resume()
}
}
I noticed that if the fourth and fifth groups were commented out, the compile time was dramatically improved. So, I started digging into them.
Often, Swift compile times are adversely affected by trying to do type inference. In this case, specifying the type of the ForEach closure parameter (see my //<-- HERE lines) seems to have fixed the compilation time.
Of course, this exact fix won't work every time, but hopefully I've outlined the strategy for how to debug it.
Note: I've left something commented out where you use MultipleSelectionRow -- make sure that if you're including code here on SO, you include all of the relevant information for someone to compile it

Having user add multiple Images to SwiftUI view

I am practicing with SwiftUI and making a meme maker which has labels that are produced from a textField and can be moved and resized. I also want to be able to do this with images from the users Photo library. I am able to get one image, but if I try and get more it just replaces the first image. I tried having the images added to an array, but then the images will not show up on the memeImageView.
Image property
#State private var image = UIImage()
Button
Button {
self.isShowPhotoLibrary = true
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
MemeUmageView
var memeImageView: some View {
ZStack {
KFImage(URL(string: meme.url ?? ""))
.placeholder {
ProgressView()
}
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: UIScreen.main.bounds.height / 2.5)
ForEach(addedLabels, id:\.self) { label in
DraggableLabel(text: label)
}
DraggableImage(image: image)
}
.clipped()
}
Attempt with using an array. I also tried making three buttons to add up to three images, each as its own property thinking that the initial property was being overridden.
My image array
#State private var addedImages = [UIImage?]()
Button
Button {
self.isShowPhotoLibrary = true
addedImages.append(image)
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
var memeImageView: some View {
ZStack {
KFImage(URL(string: meme.url ?? ""))
.placeholder {
ProgressView()
}
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: UIScreen.main.bounds.height / 2.5)
ForEach(addedLabels, id:\.self) { label in
DraggableLabel(text: label)
}
ForEach(0..<addedImages.count) { index in
DraggableImage(image: addedImages[index]!)
}
}
.clipped()
}
Where I call MemeImageView.
var body: some View {
VStack(spacing: 12) {
memeImageView
ForEach(0..<(meme.boxCount ?? 0)) { i in
TextField("Statement \(i + 1)", text: $addedLabels[i])
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.gray.opacity(0.25))
.cornerRadius(5)
.onTapGesture {
self.endEditing()
}
}
.padding(.horizontal)
}.onTapGesture {
self.endEditing()
}
// Gets a new Image
Button {
self.isShowPhotoLibrary = true
addedImages.append(image)
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
Spacer()
// Saves Image
Button {
// takes a screenshot and crops it
if let image = memeImageView.takeScreenshot(origin: CGPoint(x: 0, y: UIApplication.shared.windows[0].safeAreaInsets.top + navBarHeight + 1), size: CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 2.5)) {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
presentationMode.wrappedValue.dismiss() // dismisses the view
}
}
label: {
Text("Save image")
.foregroundColor(Color.yellow)
}.frame( width: 150, height: 50)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.red, lineWidth: 3)
)
.navigationBarTitle(meme.name ?? "Meme", displayMode: .inline)
.background(NavBarAccessor { navBar in
self.navBarHeight = navBar.bounds.height
})
}
For Reproducing(as close to how mine actual project is setup):
Content View
import SwiftUI
struct ContentView: View {
var body: some View {
DragImageView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DragImageView:
import SwiftUI
struct DragImageView: View {
//===================
// MARK: Properties
//===================
#State private var addedImages = [UIImage?]()
#State private var isShowPhotoLibrary = false
#State private var image = UIImage()
var body: some View {
VStack(spacing: 12) {
imageView
}
// Gets a new Image
Button {
self.isShowPhotoLibrary = true
addedImages.append(image)
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
Spacer()
}
var imageView: some View {
ZStack {
DraggableImage(image: image)
}
//.clipped()
}
// This will dismiss the keyboard
private func endEditing() {
UIApplication.shared.endEditing()
}
}
// Allows fot the keyboard to be dismissed
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
DraggableImage:
import SwiftUI
struct DraggableImage: View {
// Drag Gesture
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
// Roation Gesture
#State private var rotation: Double = 0.0
// Scale Gesture
#State private var scale: CGFloat = 1.0
// The different states the frame of the label could be
private enum WidthState: Int {
case full, half, third, fourth
}
#State private var widthState: WidthState = .full
#State private var currentWidth: CGFloat = 100 //UIScreen.main.bounds.width
var image: UIImage
var body: some View {
VStack {
Image(uiImage: self.image)
.resizable()
.scaledToFill()
.frame(width: self.currentWidth)
.lineLimit(nil)
}
.scaleEffect(scale) // Scale based on our state
.rotationEffect(Angle.degrees(rotation)) // Rotate based on the state
.offset(x: self.currentPosition.width, // Offset from the drag difference from it's current position
y: self.currentPosition.height)
.gesture(
// Two finger rotation
RotationGesture()
.onChanged { angle in
self.rotation = angle.degrees // keep track of the angle for state
}
// We want it to work with the scale effect, so they could either scale and rotate at the same time
.simultaneously(with:
MagnificationGesture()
.onChanged { scale in
self.scale = scale.magnitude // Keep track of the scale
})
// Update the drags new position to be wherever it was last dragged to. (we don't want to reset it back to it's current position)
.simultaneously(with: DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width,
height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.newPosition = self.currentPosition
})
)
/// Have to do double tap first or else it will never work with the single tap
.onTapGesture(count: 2) {
// Update our widthState to be the next on in the 'enum', or start back at .full
self.widthState = WidthState(rawValue: self.widthState.rawValue + 1) ?? .full
self.currentWidth = UIScreen.main.bounds.width / CGFloat(self.widthState.rawValue)
}
}
}
ImagePicker:
import UIKit
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var selectedImage: UIImage
#Environment(\.presentationMode) private var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.allowsEditing = false
imagePicker.sourceType = sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
parent.selectedImage = image
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
I should add this is to make memes, so the user picked images go on top the view that I save to the camera roll.
I'm not 100% clear on what the exact desired output should be, but this should get you started (explained below):
struct DragImageView: View {
//===================
// MARK: Properties
//===================
#State private var addedImages = [UIImage]()
#State private var isShowPhotoLibrary = false
var bindingForImage: Binding<UIImage> {
Binding<UIImage> { () -> UIImage in
return addedImages.last ?? UIImage()
} set: { (newImage) in
addedImages.append(newImage)
print("Images: \(addedImages.count)")
}
}
var body: some View {
VStack(spacing: 12) {
imageView
}
// Gets a new Image
Button {
self.isShowPhotoLibrary = true
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: bindingForImage)
}
Spacer()
}
var imageView: some View {
VStack {
ForEach(addedImages, id: \.self) { image in
DraggableImage(image: image)
}
}
}
// This will dismiss the keyboard
private func endEditing() {
UIApplication.shared.endEditing()
}
}
addedImages is now an array of non-optional UIImages
There's a custom Binding for the image picker. When it receives a new image, it appends it to the end of the array.
In var imageView, there's a VStack instead of a ZStack so that multiple images can get displayed (instead of stacked on top of each other) and a ForEach loop to iterate through the images.

SwiftUI: loading images with .fileImporter

Goal is to load 2 different images (image 1 and 2) with the new .fileImporter modifier.
Problem is I get the same image loaded to both thumbnails (image 1 and 2).
Have anyone managed to do that with .fileImporter modifier?
import SwiftUI
struct ContentView: View {
#State var openFile = false
#State var img1 = UIImage()
#State var img2 = UIImage()
#State var fileName = ""
var body: some View {
Form {
//image 1
Button(action: {
self.openFile.toggle()
}){
Image(uiImage: self.img1)
.renderingMode(.original)
.resizable()
.frame(width: 48, height: 48)
.clipShape(Circle())
}
//image 2
Button(action: {
self.openFile.toggle()
}){
Image(uiImage: self.img2)
.renderingMode(.original)
.resizable()
.frame(width: 48, height: 48)
.clipShape(Circle())
}
}
.navigationTitle("File Importer")
//file importer
.fileImporter(isPresented: $openFile, allowedContentTypes: [.image]) { (res) in
do{
let fileUrl = try res.get()
print(fileUrl)
self.fileName = fileUrl.lastPathComponent
fileUrl.startAccessingSecurityScopedResource()
if let imageData = try? Data(contentsOf: fileUrl),
let image = UIImage(data: imageData) {
self.img1 = image
self.img2 = image
}
fileUrl.stopAccessingSecurityScopedResource()
} catch{
print ("error reading")
print (error.localizedDescription)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Well... I would move file importer into separated view to use binding depending on which button tapped.
Update: worked variant for Form. Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
#State private var openFile = false
#State private var img1 = UIImage()
#State private var img2 = UIImage()
#State private var target: Binding<UIImage>? // dynamic target for importer
var body: some View {
Form {
//image 1
Button(action: {
self.target = $img1
self.openFile.toggle()
}){
Image(uiImage: self.img1)
.renderingMode(.original)
.resizable()
.frame(width: 48, height: 48)
.clipShape(Circle())
}
//image 2
Button(action: {
self.target = $img2
self.openFile.toggle()
}){
Image(uiImage: self.img2)
.renderingMode(.original)
.resizable()
.frame(width: 48, height: 48)
.clipShape(Circle())
}
}
.navigationTitle("File Importer")
//file importer
.fileImporter(isPresented: $openFile, allowedContentTypes: [.image]) { (res) in
do{
let fileUrl = try res.get()
print(fileUrl)
guard fileUrl.startAccessingSecurityScopedResource() else { return }
if let imageData = try? Data(contentsOf: fileUrl),
let image = UIImage(data: imageData) {
self.target?.wrappedValue = image
}
fileUrl.stopAccessingSecurityScopedResource()
} catch{
print ("error reading")
print (error.localizedDescription)
}
}
}
}
Here is possible solution (kept just in case), but not for Form:
struct ImportContentView: View {
#State var openFile = false
#State var img1 = UIImage()
#State var img2 = UIImage()
var body: some View {
//Form { // << does not work for Form !!
VStack {
//image 1
Button(action: {
self.openFile.toggle()
}){
Image(uiImage: self.img1)
.renderingMode(.original)
.resizable()
.frame(width: 48, height: 48)
.clipShape(Circle())
.background(LoaderView(isActive: $openFile, image: $img1))
}
//image 2
Button(action: {
self.openFile.toggle()
}){
Image(uiImage: self.img2)
.renderingMode(.original)
.resizable()
.frame(width: 48, height: 48)
.clipShape(Circle())
.background(LoaderView(isActive: $openFile, image: $img2))
}
}
.navigationTitle("File Importer")
}
}
struct LoaderView: View {
#Binding var isActive: Bool
#Binding var image: UIImage
var body: some View {
Color.clear
.fileImporter(isPresented: $isActive, allowedContentTypes: [.image]) { (res) in
do{
let fileUrl = try res.get()
print(fileUrl)
guard fileUrl.startAccessingSecurityScopedResource() else { return }
if let imageData = try? Data(contentsOf: fileUrl),
let image = UIImage(data: imageData) {
self.image = image
}
fileUrl.stopAccessingSecurityScopedResource()
} catch{
print ("error reading")
print (error.localizedDescription)
}
}
}
}
This solution works for Form.
struct FileImporterView: View {
#State var openFile = false
#State var images = [UIImage(), UIImage()]
#State var index = 0
var body: some View {
Form {
ForEach(Array(0..<images.count), id: \.self) { id in
Button(action:{}) {
ImageRow(isPresenting: $openFile, img: $images[id], index: $index, id: id)
}
}
}
.navigationTitle("File Importer")
.fileImporter(isPresented: $openFile, allowedContentTypes: [.image], onCompletion: importImage)
}
func importImage(_ res: Result<URL, Error>) {
do{
let fileUrl = try res.get()
print(fileUrl)
guard fileUrl.startAccessingSecurityScopedResource() else { return }
if let imageData = try? Data(contentsOf: fileUrl),
let image = UIImage(data: imageData) {
self.images[index] = image
}
fileUrl.stopAccessingSecurityScopedResource()
} catch{
print ("error reading")
print (error.localizedDescription)
}
}
}
struct ImageRow: View {
#Binding var isPresenting: Bool
#Binding var img: UIImage
#Binding var index: Int
var id: Int
init(isPresenting: Binding<Bool>, img: Binding<UIImage>, index: Binding<Int>, id: Int) {
self._isPresenting = isPresenting
self._img = img
self._index = index
self.id = id
}
var body: some View {
HStack {
Image(uiImage: img)
.renderingMode(.original)
.resizable()
.frame(width: 48, height: 48)
.clipShape(Circle())
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
self.index = id
self.isPresenting = true
}
}
}

How to extend tappable area to whole frame in SwiftUI (iOS14)?

I am trying to make a VGrid with Swift 5.3, but the only tappable area is the upper part of the rectangle. Other answers suggest contentShape, but I am unable to make that work either. How to make the whole frame tappable? Code below:
import SwiftUI
import Combine
import Foundation
struct Item: Codable, Identifiable, Equatable {
var id: Int
var name: String
}
final class UserData: ObservableObject {
#Published var items = Bundle.main.decode([Item].self, from: "data.json")
}
struct ContentView: View {
#State var itemID = Item.ID()
#StateObject var userData = UserData()
let columns = [
GridItem(.adaptive(minimum: 118))
]
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(userData.items) { item in
NavigationLink(destination: ContentDetail(itemID: item.id - 1)) {
ContentRow(item: item)
}
}
}
}
}
}
}
struct ContentRow: View {
var item: Item
var body: some View {
VStack {
GeometryReader { geo in
ZStack{
VStack(alignment: .trailing) {
Text(item.name)
.font(.caption)
}
}
.padding()
.foregroundColor(Color.primary)
.frame(width: geo.size.width, height: 120)
.border(Color.primary, width: 2)
.cornerRadius(5)
.contentShape(Rectangle())
}
}
}
}
struct ContentDetail: View {
#State var itemID = Item.ID()
#StateObject var userData = UserData()
var body: some View {
Text(userData.items[itemID].name)
}
}
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from file: String, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateDecodingStrategy
decoder.keyDecodingStrategy = keyDecodingStrategy
do {
return try decoder.decode(T.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
fatalError("Failed to decode \(file) from bundle due to missing key '\(key.stringValue)' not found – \(context.debugDescription)")
} catch DecodingError.typeMismatch(_, let context) {
fatalError("Failed to decode \(file) from bundle due to type mismatch – \(context.debugDescription)")
} catch DecodingError.valueNotFound(let type, let context) {
fatalError("Failed to decode \(file) from bundle due to missing \(type) value – \(context.debugDescription)")
} catch DecodingError.dataCorrupted(_) {
fatalError("Failed to decode \(file) from bundle because it appears to be invalid JSON")
} catch {
fatalError("Failed to decode \(file) from bundle: \(error.localizedDescription)")
}
}
}
And the JSON part:
[
{
"id": 1,
"name": "Example data",
},
{
"id": 2,
"name": "Example data 2",
}
]
Any help is appreciated. Could this be a bug in SwiftUI?
You could simply remove the GeometryReader since you set the height anyway:
struct ContentRow: View {
var item: Item
var body: some View {
VStack {
ZStack{
VStack(alignment: .trailing) {
Text(item.name)
.font(.caption)
}
}
.padding()
.foregroundColor(Color.primary)
.frame(width: 120, height: 120)
.border(Color.primary, width: 2)
.cornerRadius(5)
.background(Color.red)
}
}
}
Try putting the contentShape on the outermost VStack of ContentRow. You need to put the contentShape on the view that is expanding to fill its parent (or on its parent), which in your case I think is the GeometryReader. The views inside the GeometryReader all shrink to fit their contents, so your contentShape rectangle doesn’t help there.

Swiftui ObservableObject with array not update view

I have a problem with Array using ObservableObject in my view. I have an empty array. I call a function at page onAppear. When the data is returned, the view does not update with the new data in array:
class NewsState: ObservableObject {
private let base: String = "api"
let objectWillChange = ObservableObjectPublisher()
#Published var wagsList: Array<UserSlider> = [] {
willSet {
objectWillChange.send()
}
}
func getList() {
let url = NSURL(string: "\(base)/UserApi/getList")
var mutableURLRequest = URLRequest(url: url! as URL)
mutableURLRequest.httpMethod = "GET"
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
AF.request(mutableURLRequest).responseData { response in
guard let data = response.data else { return }
let resp = try! JSONDecoder().decode(Array<UserSlider>.self, from: data)
for i in resp {
let userSlider = UserSlider(id: i.id, uid: i.uid, image: i.image)
self.wagsList.append(userSlider)
}
}
}
}
In my view I have this:
HStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
if(self.newsState.wagsList.count != 0) {
ForEach(self.newsState.wagsList, id: \.self) { wags in
VStack {
HStack {
URLImage(URL(string: "\(wags.image)")!, expireAfter: Date(timeIntervalSinceNow: 10)) { proxy in
proxy.image
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.clipShape(Circle())
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 2)
)
.contentShape(Circle())
}.frame(width: 62, height: 62)
}
HStack {
Text("10K")
.foregroundColor(Color.white)
.font(Font.custom("Metropolis-Bold", size: 15))
}
HStack {
Text("followers")
.foregroundColor(Color.white)
.font(Font.custom("Metropolis-Normal", size: 15))
}
}
}
} else {
//loader
}
}.onAppear(perform: initPage)
}
}
What am I doing wrong? I see that the problem is caused by ScrollView.
Try this one
class NewsState: ObservableObject {
private let base: String = "api"
#Published var wagsList: Array<UserSlider> = []
func getList() {
let url = NSURL(string: "\(base)/UserApi/getList")
var mutableURLRequest = URLRequest(url: url! as URL)
mutableURLRequest.httpMethod = "GET"
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
AF.request(mutableURLRequest).responseData { response in
guard let data = response.data else { return }
let resp = try! JSONDecoder().decode(Array<UserSlider>.self, from: data)
let results = resp.map { UserSlider(id: $0.id, uid: $0.uid, image: $0.image) }
DispatchQueue.main.async {
self.wagsList = results
}
}
}
}
As it is not clear to me where the error might lay. It could be either in getList or in your View.
This is an easy example of how it works with a Published and ObserverdObject:
Note: your getList function is not in this solution as the error could be with your API, JSON ect.
import SwiftUI
struct ContentView: View {
#ObservedObject var state = NewsState()
var body: some View {
Group { //needed for the IF Statement below
if state.stringList.count > 0 {
ForEach(self.state.stringList, id: \.self){ s in
Text(String(s))
}
}
}.onTapGesture {
self.state.getNewList()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class NewsState: ObservableObject {
#Published var stringList: Array<String> = []
init() {
self.getList()
}
func getList() {
self.stringList.append("New")
}
func getNewList() {
self.stringList = []
self.stringList.append("New new")
}
}