PhotosPicker inside ForEach get called multiple times onChange - ios

I'm building an app to let user create section and upload photos to each section. I have PhotosPicker() inside ForEach sections array
ForEach(sections,id: \.self){ section in
PhotosPicker(
selection: $selectedItem,
matching: .images,
photoLibrary: .shared()) {
...
}
.onChange(of: selectedItem) { newItem in
//This get called multiple times (for each section) even tap on one
}
}
So the idea is that the user can upload a photo to each section inside the for each. But when I do it like this, it triggers the photo picker for each section and ends up uploading the same photo to the same section.
Is there any way to make make the PhotosPicker call once?
Thank you so much in advance for your help!

Each section needs its own selectedItem so you have to put the PhotosPicker in a subview.
import PhotosUI
#available(iOS 16.0, *)
struct ReusablePhotoPickerView: View{
//Each row in the ForEach needs its own `#State` right now you are sharing one variable for all the sections.
#State private var selectedItem: PhotosPickerItem?
//Pass the UIImage to the section
let onSelected: (Result<UIImage, Error>) -> Void
var body: some View{
PhotosPicker(
selection: $selectedItem,
matching: .images,
photoLibrary: .shared()) {
Image(systemName: "plus.app")
}.task(id: selectedItem, {
do{
let result = try await selectedItem?.getImage()
switch result {
case .success(let success):
onSelected(.success(success))
case .failure(let failure):
onSelected(.failure(failure))
case .none:
onSelected(.failure(AppError.noImageFound))
}
}catch{
onSelected(.failure(error))
}
})
}
}
#available(iOS 16.0, *)
extension PhotosPickerItem{
func getImage() async throws -> Result<UIImage, Error>{
let data = try await self.loadTransferable(type: Data.self)
guard let data = data, let image = UIImage(data: data) else{
return .failure(AppError.noImageFound)
}
return .success(image)
}
}
enum AppError: LocalizedError{
case noImageFound
}
Once you have decoupled the section process you can assign the UIImage to the specific section.
import SwiftUI
#available(iOS 16.0, *)
struct MultiPhotoPicker: View {
#State var sections: [ImageModel] = [.init(description: "flowers"), .init(description: "trees")]
var body: some View {
VStack{
ForEach($sections) { $section in
Text(section.description)
HStack{
Spacer()
ForEach($section.images, id:\.cgImage) { $image in
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(height: 40)
}
Spacer()
ReusablePhotoPickerView { result in
switch result {
case .success(let success):
//Adding the image to the section.images array
$section.wrappedValue.images.append(success)
case .failure(let failure):
//You should always show errors to the user.
print("Error while adding images to \(section.description) \n\(failure)")
}
}
}
}
}
}
struct ImageModel: Identifiable{
let id: UUID = .init()
var images: [UIImage] = []
var description: String
}
}

Related

List with editmode and onAppear

I am trying to display the list of items in editmode after the api call but List does not enter editmode. As a result, regular list of items is shown (not able to select an item). When I have static values, everything works well.
The view:
struct Step1: View {
#ObservedObject var page: Page
var body: some View {
VStack {
List(page.items, id: \.id, selection: $page.selectedItemStr) { item in
Text(item.name ?? "")
}
.listStyle(.plain)
.environment(\.editMode, $page.editMode)
.onAppear {
page.getItemsAsync()
}
}
}
}
editMode variable:
#Published var editMode: EditMode = .inactive
fetching data from api
func getItemsAsync() {
ItemService.getItemsAsyncMain() { (result) in
DispatchQueue.main.sync {
switch result {
case .success(let succes):
self.items = succes.results
self.hasMore = succes.hasMore
self.editMode = .active
case .failure(let failure):
self.items = []
print(failure)
}
}
}
}
I would appreciate any suggestions on why my code does not work. Thank you in advance

SwiftUI TabView PageTabViewStyle crashes without showing any error

I am trying to create an Image carousel using tabview and loading pictures from firebase. Without showing any error message or code tabview crashing. Please shed some light on what's going wrong here.
struct HomeView : View{
var body : View{
NavigationView{
VStack{
ScrollView{
CategoryView(homeViewModel: homeViewModel)
PosterView(homeViewModel: homeViewModel)
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarTitle("")
}
}
}
struct PosterView : View {
#StateObject var homeViewModel : HomeViewModel = HomeViewModel()
#State var currentIndex: Int = 0
var timer = Timer.publish(every: 3, on: .main, in: .common)
func next(){
withAnimation{
currentIndex = currentIndex < homeViewModel.posterList.count ? currentIndex +
1 : 0
}
}
var body: some View{
Divider()
GeometryReader{ proxy in
VStack{
TabView(selection: $currentIndex){
ForEach(homeViewModel.posterList){ item in
let imgURL = homeViewModel.trendingImgDictionary[item.id ?? ""]
AnimatedImage(url: URL(string: imgURL ?? ""))
}
}.tabViewStyle(PageTabViewStyle())
.padding()
.frame(width: proxy.size.width, height: proxy.size.height)
.onReceive(timer) { _ in
next()
}
.onTapGesture {
print("Tapped")
}
}
}
}
}
ViewModel: It contains two methods to fetch data and pictures from Firebase. That's working fine and am getting proper data. The only issue is while displaying it tabview crashes without showing any error messages.
class HomeViewModel : ObservableObject {
#Published var posterList : [TrendingBanner] = []
#Published var trendingImgDictionary : [String : String] = [:]
init() {
self.fetchTrendingList()
}
func fetchTrendingList() {
self.posterList.removeAll()
firestore.collection(Constants.COL_TRENDING).addSnapshotListener { snapshot, error in
guard let documents = snapshot?.documents else{
print("No Documents found")
return
}
self.posterList = documents.compactMap({ (queryDocumentSnapshot) -> TrendingBanner? in
return try? queryDocumentSnapshot.data(as:TrendingBanner.self )
})
print("Trending list \(self.posterList.count)")
print(self.posterList.first?.id)
let _ = self.posterList.map{ item in
self.LoadTrendingImageFromFirebase(id: item.id ?? "")
}
}
}
func LoadTrendingImageFromFirebase(id : String) {
let storageRef = storageRef.reference().child("trending/\(id)/\(id).png")
storageRef.downloadURL { (url, error) in
if error != nil {
print((error?.localizedDescription)!)
return
}
self.trendingImgDictionary[id] = url!.absoluteString
print("Trending img \(self.trendingImgDictionary)")
}
}
If you open SwiftUI module sources, you'll see the comment on top of TabView:
Tab views only support tab items of type Text, Image, or an image
followed by text. Passing any other type of view results in a visible but
empty tab item.
You're using AnimatedImage which is probably not intended to be supported by TabView.
Update
I made a library that liberates the SwiftUI _PageView which can be used to build a nice tab bar. Check my story on that.
Had the same issue recently on devices running iOS 14.5..<15. Adding .id() modifier to the TabView solved it.
Example:
TabView(selection: $currentIndex) {
ForEach(homeViewModel.posterList) { item in
content(for: item)
}
}
.tabViewStyle(PageTabViewStyle())
.id(homeViewModel.posterList.count)

How to pass data to a sub view from ContentView in SwiftUI

Forgive me if this doesn't make sense, I am a total beginner at Swift. I am creating a recipe app that pulls data from an API and lists it out in navigation links. When the user clicks on the recipe I want it to move to sub view and display information from the API such as recipe name, image, ingredients, and have a button with a link to the webpage.
I was able to get the data pulled into the list with navigation links. However, now I do not know how to go about setting up the recipe details sub view with all of the information I listed above.
This is where I call the API:
class RecipeService {
func getRecipes(_ completion: #escaping (Result<[Recipe], Error>) -> ()) {
let url = URL(string: "http://www.recipepuppy.com/api")!
URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error {
return completion(.failure(error))
}
guard let data = data else {
return completion(.failure(NSError(domain: "", code: -1, userInfo: nil)))
}
do {
let response = try JSONDecoder().decode(RecipesResponses.self, from: data)
completion(.success(response.results))
} catch {
completion(.failure(error))
}
}.resume()
}
}
This is where I take in the recipe responses:
struct RecipesResponses: Codable {
let title: String
let version: Double
let href: String
let results: [Recipe]
}
struct Recipe: Codable {
let title, href, ingredients, thumbnail: String
var detail: URL {
URL(string: href)!
}
var thumb: URL {
URL(string: thumbnail)!
}
}
This is my recipe ViewModel:
class RecipeViewModel: ObservableObject {
#Published var recipes = [Recipe]()
#Published var isLoading = false
private let service = RecipeService()
init() {
loadData()
}
private func loadData() {
isLoading = true
service.getRecipes{ [weak self] result in
DispatchQueue.main.async {
self?.isLoading = false
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let recipes):
self?.recipes = recipes
}
}
}
}
}
This is my view where I list out the API responses:
struct ListView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
NavigationView {
List(viewModel.recipes, id: \.href) { recipe in
NavigationLink (destination: RecipeDetailView()) {
HStack{
CachedImageView(recipe.thumb)
.mask(Circle())
.frame(width: 80)
VStack(alignment: .leading) {
Text(recipe.title)
.font(.largeTitle)
.foregroundColor(.black)
.padding()
}
}
.buttonStyle(PlainButtonStyle())
}
}
.navigationBarTitle(Text("All Recipes"))
}
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
This is the view where I would like to list out the recipe details and link to the webpage. This is where I am struggling to be able to pull the API data into:
struct RecipeDetailView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
Text("Detail View")
}
}
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView()
}
}
Images of app
You can change RecipeDetailView to accept a Recipe as a parameter:
struct RecipeDetailView: View {
var recipe : Recipe
var body: some View {
Text(recipe.title)
Link("Webpage", destination: recipe.detail)
//etc
}
}
Then, in your NavigationLink, pass the Recipe to it:
NavigationLink(destination: RecipeDetailView(recipe: recipe)) {
One thing I'd warn you about is force unwrapping the URLs in Recipe using ! -- you should know that if you ever get an invalid/malformed URL, this style of unwrapping will crash the app.
Update, to show you what the preview might look like:
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView(recipe: Recipe(title: "Recipe name", href: "https://google.com", ingredients: "Stuff", thumbnail: "https://linktoimage.com"))
}
}

SwiftUI TextField not updating in sibling View (with video)

I have a simple todo list demo app I built to understand the relationship between SwiftUI and Core Data. When I modify a Task in the TaskDetail view the changes are not reflected within the TextField residing in the TaskRow view. Both of these views are children of the ContentView.
Sudo Fix: If I change out TextField for Text the view is updated as expected; but, I need to edit the title attribute in Task from the row.
2nd Option: It seems like every tutorial avoids updating data inside a child view using Core Data. I can use #EnvironmentObject to sync data across views easily (with structs). However, keeping the environment data and the Core Data store synced sounds like a nightmare. I'd expect there to be an easier way :D
Video of Issue: https://youtu.be/JV-jQHpXE4Y
Code
ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: Task.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Task.position, ascending: true)]) var tasks: FetchedResults<Task>
init() {
print("INIT - Content View")
}
var body: some View {
NavigationView {
VStack {
todoList
newButton
}
}
}
}
extension ContentView {
var todoList: some View {
List {
ForEach(self.tasks, id: \.id) { task in
NavigationLink(destination: TaskDetail(task: task)) {
TaskRow(task: task)
}
}
.onDelete { indices in
for index in indices {
self.context.delete(self.tasks[index])
try? self.context.save()
}
}
.onMove(perform: move)
}
.navigationBarItems(trailing: EditButton())
}
var newButton: some View {
Button(action: {
self.newTask()
}, label: {
Text("Add Random Task")
}).padding([.bottom, .top], 20)
}
}
extension ContentView {
private func newTask() {
let things = ["Cook", "Clean", "Eat", "Workout", "Program"]
let newItem = Task(context: self.context)
newItem.id = UUID()
newItem.title = things.randomElement()!
newItem.position = Int64(self.tasks.count)
newItem.completed = Bool.random()
try? self.context.save()
}
private func move(from source: IndexSet, to destination: Int) {
// Make an array of items from fetched results
var revisedItems: [Task] = self.tasks.map{ $0 }
// change the order of the items in the array
revisedItems.move(fromOffsets: source, toOffset: destination )
// update the userOrder attribute in revisedItems to
// persist the new order. This is done in reverse order
// to minimize changes to the indices.
for reverseIndex in stride(from: revisedItems.count - 1, through: 0, by: -1) {
revisedItems[reverseIndex].position = Int64(reverseIndex)
try? self.context.save()
}
}
}
TaskRow.swift
import SwiftUI
import CoreData
struct TaskRow: View {
#Environment(\.managedObjectContext) var context
#ObservedObject var task: Task
#State private var title: String
init(task: Task) {
self.task = task
self._title = State(initialValue: task.title ?? "")
print("INIT - TaskRow Initialized: title=\(title), completed=\(task.completed)")
}
var body: some View {
HStack {
TextField(self.task.title ?? "", text: self.$title) {
self.task.title = self.title
self.save()
}.foregroundColor(.black)
// Text(self.task.title ?? "")
Spacer()
Text("\(self.task.position)")
Button(action: {
self.task.completed.toggle()
self.save()
}, label: {
Image(systemName: self.task.completed ? "checkmark.square" : "square")
}).buttonStyle(BorderlessButtonStyle())
}
}
}
extension TaskRow {
func save() {
try? self.context.save()
print("SAVE - TaskRow")
}
}
TaskDetail.swift
import SwiftUI
struct TaskDetail: View {
#Environment(\.managedObjectContext) var context
#ObservedObject var task: Task
#State private var title: String
init(task: Task) {
self.task = task
self._title = State(initialValue: task.title ?? "")
print("INIT - TaskDetail Initialized: title=\(title), completed=\(task.completed)")
}
var body: some View {
Form {
Section {
TextField(self.title, text: self.$title) {
self.task.title = self.title
self.save()
}.foregroundColor(.black)
}
Section {
Button(action: {
self.task.completed.toggle()
self.save()
}, label: {
Image(systemName: self.task.completed ? "checkmark.square" : "square")
}).buttonStyle(BorderlessButtonStyle())
}
}
}
}
extension TaskDetail {
func save() {
try? self.context.save()
print("SAVE - TaskDetail")
}
}
Core Data Model of Task
Edit
This has to do with the 'PlaceHolder' text (first argument) within the TextField. If I modify the Task in TaskDetail and then navigate back to ContentView it doesn't appear to update. But, if I remove the text in the row (highlight, backspace) the 'PlaceHolder' text contains the updated value.
What's strange is that exiting the app and restarting it displays the changes made in the TextField with dark font (expected behavior without the restart).
Try the following
var body: some View {
HStack {
TextField(self.task.title ?? "", text: self.$title) {
self.task.title = self.title
self.save()
}.foregroundColor(.black)
.onReceive(task.objectWillChange) { _ in // << here !!
if task.title != self.title {
task.title = self.title
}
}
It's fun to play with SwiftUI (I have no experience with it). But what I can see in most of the questions in different forums about TextField, the binding value can be created by .constant. Therefore, use this:
TextField(self.task.title ?? "", text: .constant(self.task.title!))
This should work now.
Demo in GIF:
Use #Binding instead of #State
It is important to remember that TextField is actually a SwiftUI View (via inheritance). The parent child relationship is actually TaskRow -> TextField.
#State is used for representing the 'state' of a view. While this value can be passed around, it's not meant to be written to by other views (it has a single source of truth).
In the case above, I am actually passing title (via $ prefix) to another view while expecting either the parent or child to modify the title property. #Binding supports 2 way communication between views or a property and view.
#State Apple Docs: https://developer.apple.com/documentation/swiftui/state
#Binding Apple Docs: https://developer.apple.com/documentation/swiftui/binding
Jared Sinclair's Wrapper Rules: https://jaredsinclair.com/2020/05/07/swiftui-cheat-sheet.html
Changing the TaskRow and TaskDetail views fixed the behavior:
TaskRow.swift
import SwiftUI
import CoreData
struct TaskRow: View {
#Environment(\.managedObjectContext) var context
#ObservedObject var task: Task
#Binding private var title: String
init(task: Task) {
self.task = task
self._title = Binding(get: {
return task.title ?? ""
}, set: {
task.title = $0
})
print("INIT - TaskRow Initialized: title=\(task.title ?? ""), completed=\(task.completed)")
}
var body: some View {
HStack {
TextField("Task Name", text: self.$title) {
self.save()
}.foregroundColor(.black)
Spacer()
Text("\(self.task.position)")
Button(action: {
self.task.completed.toggle()
self.save()
}, label: {
Image(systemName: self.task.completed ? "checkmark.square" : "square")
}).buttonStyle(BorderlessButtonStyle())
}
}
}
extension TaskRow {
func save() {
try? self.context.save()
print("SAVE - TaskRow")
}
}
TaskDetail.swift
import SwiftUI
struct TaskDetail: View {
#Environment(\.managedObjectContext) var context
#ObservedObject var task: Task
#Binding private var title: String
init(task: Task) {
self.task = task
self._title = Binding(get: {
return task.title ?? ""
}, set: {
task.title = $0
})
print("INIT - TaskDetail Initialized: title=\(task.title ?? ""), completed=\(task.completed)")
}
var body: some View {
Form {
Section {
TextField("Task Name", text: self.$title) {
self.save()
}.foregroundColor(.black)
}
Section {
Button(action: {
self.task.completed.toggle()
self.save()
}, label: {
Image(systemName: self.task.completed ? "checkmark.square" : "square")
}).buttonStyle(BorderlessButtonStyle())
}
}
}
}
extension TaskDetail {
func save() {
try? self.context.save()
print("SAVE - TaskDetail")
}
}

Trouble displaying data from Contentful in SwiftUI

I'm trying to display data from Contentful into my SwiftUI app but I'm hitting an issue.
The goal is to display a list of movies and have them tappable. When I select a movie I want to get the data for that movie, so the title & movie trailer for example.
But in my selectable row I'm getting Use of undeclared type 'movie' and in my movies View I'm getting Use of undeclared type 'fetcher'
Here's what I have tried below:
import SwiftUI
import Combine
import Contentful
struct Movie: Codable, Identifiable, FieldKeysQueryable, EntryDecodable {
static let contentTypeId: String = "movies"
// FlatResource Memberes.
let id: String
var updatedAt: Date?
var createdAt: Date?
var localeCode: String?
var title: String
var movieId: String
var movieTrailer: String
enum FieldKeys: String, CodingKey {
case title, movieId, movieTrailer
}
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case movieId = "movieId"
case movieTrailer = "movieTrailer"
}
}
public class MovieFetcher: ObservableObject {
#Published var movies = [Movie]()
init() {
getArray(id: "movies") { (items) in
items.forEach { (item) in
self.movies.append(Movie(id: item.id, title: item.title, movieId: item.movieId, movieTrailer: item.movieTrailer))
}
}
}
func getArray(id: String, completion: #escaping([Movie]) -> ()) {
let client = Client(spaceId: spaceId, accessToken: accessToken, contentTypeClasses: [Movie.self])
let query = QueryOn<Movie>.where(contentTypeId: "movies")
client.fetchArray(of: Movie.self, matching: query) { (result: Result<ArrayResponse<Movie>>) in
switch result {
case .success(let array):
DispatchQueue.main.async {
completion(array.items)
}
case .error(let error):
print(error)
}
}
}
}
struct moviesView : View {
#ObservedObject var fetcher = MovieFetcher()
#State var selectMovie: Movie? = nil
#Binding var show: Bool
var body: some View {
HStack(alignment: .bottom) {
if show {
ScrollView(.horizontal) {
Spacer()
HStack(alignment: .bottom, spacing: 30) {
ForEach(fetcher.movies, id: \.self) { item in
selectableRow(movie: item, selectMovie: self.$selectMovie)
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding(.leading, 46)
.padding(.bottom, 26)
}
}
}
}
struct selectableRow : View {
var movie: Movie
#Binding var selectedMovie: Movie?
#State var initialImage = UIImage()
var urlString = "\(urlBase)\(movie.movieId).png?"
var body: some View {
ZStack(alignment: .center) {
if movie == selectedMovie {
Image("")
.resizable()
.frame(width: 187, height: 254)
.overlay(
RoundedRectangle(cornerRadius: 13)
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 182, height: 249)
.onAppear {
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
} else {
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 135, height: 179)
.onAppear {
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
}
}
.onTapGesture {
self.selectedMovie = self.movie
}
}
}
I suppose it was intended
struct moviesView : View {
#ObservedObject var fetcher = MovieFetcher()
#State var selectMovie: Movie? = nil // type is Movie !!
...
and here
struct selectableRow : View {
var movie: Movie
#Binding var selectedMovie: Movie? // type is Movie !!
The good practice is to use Capitalized names for types and lowerCased types for variables/properties, following this neither you nor compiler be confused.

Resources