List with editmode and onAppear - ios

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

Related

PhotosPicker inside ForEach get called multiple times onChange

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

SwiftUI TabView - run code in subview after sequential taps

I am trying to implement the behavior in a TabView when the user taps the same tab multiple times, such as in the iOS AppStore app. First tap: switch to that view, second tap: pop to root, third tap: scroll to the top if needed.
The code below works fine for switching and didTap() is called for every tap.
import SwiftUI
enum Tab: String {
case one
case two
}
struct AppView: View {
#State private var activeTab = Tab.one
var body: some View {
TabView(selection: $activeTab.onChange(didTap)) {
One()
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
Two()
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
func didTap(to value: Tab) {
print(value) // this captures every tap
}
}
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler(newValue)
}
)
}
}
What I am struggling with, is how to tell either One or Two that it was tapped for a second or third time? (How to pop and scroll is not the issue).
I have seen this: TabView, tabItem: running code on selection or adding an onTapGesture but it doesn't explain how to run code in one of the views.
Any suggestions?
You can record additional taps (of same value) in an array. The array count gives you the number of taps on the same Tab.
EDIT: now with explicit subview struct.
struct ContentView: View {
#State private var activeTab = Tab.one
#State private var tapState: [Tab] = [Tab.one] // because .one is default
var body: some View {
TabView(selection: $activeTab.onChange(didTap)) {
SubView(title: "One", tapCount: tapState.count)
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
SubView(title: "Two", tapCount: tapState.count)
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
func didTap(to value: Tab) {
print(value) // this captures every tap
if tapState.last == value {
tapState.append(value) // apped next tap if same value
print("tapped \(tapState.count) times")
} else {
tapState = [value] // reset tap state to new tab selection
}
}
}
struct SubView: View {
let title: String
let tapCount: Int
var body: some View {
VStack {
Text("Subview \(title)").font(.title)
Text("tapped \(tapCount) times")
}
}
}
Although the answer by #ChrisR did answer my question, I couldn't figure out the next step, i.e. the logic when to pop-to-root or scroll-to-the-top based on the number of taps for a SubView. After lots of reading and trial and error, I recently came across this article: https://notificare.com/blog/2022/11/25/a-better-tabview-in-swiftui/
Inspired by this article, but with some modifications, I came up with the following which does exactly what I was looking for.
The two main changes are:
An EmptyView with an id is added as the first (but invisible) row in the List to be used as an anchor by proxy.scrollTo().
Instead of the global #StateObject var appState that stores the navigation paths for the subviews, I added the paths as separate #State properties. This avoids the Update NavigationAuthority bound path tried to update multiple times per frame. warning.
Hopefully this is helpful for someone.
enum Tab: String {
case one
case two
}
struct ContentView: View {
#State var selectedTab = Tab.one
#State var oneNavigationPath = NavigationPath()
#State var twoNavigationPath = NavigationPath()
var body: some View {
ScrollViewReader { proxy in
TabView(selection: tabViewSelectionBinding(proxy: proxy)) {
SubView(title: "One", path: $oneNavigationPath)
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
SubView(title: "Two", path: $twoNavigationPath)
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
}
private func tabViewSelectionBinding(proxy: ScrollViewProxy) -> Binding<Tab> {
Binding<Tab>(
get: { selectedTab },
set: { newValue in
if selectedTab == newValue {
switch selectedTab {
case .one:
if oneNavigationPath.isEmpty {
withAnimation {
proxy.scrollTo(Tab.one, anchor: .bottom)
}
} else {
withAnimation {
oneNavigationPath = NavigationPath()
}
}
case .two:
if twoNavigationPath.isEmpty {
withAnimation {
proxy.scrollTo(Tab.two, anchor: .bottom)
}
} else {
withAnimation {
twoNavigationPath = NavigationPath()
}
}
}
}
selectedTab = newValue
}
)
}
}
struct SubView: View {
let title: String
let items = Array(1 ... 100)
#Binding var path: NavigationPath
var body: some View {
NavigationStack(path: $path) {
List {
EmptyView()
.id(Tab(rawValue: title.lowercased()))
ForEach(items, id: \.self) { item in
NavigationLink(value: item) {
Text("Item \(item)")
}
}
}
.navigationTitle(title)
.navigationDestination(for: Int.self) { item in
Text("Item \(item)")
}
}
}
}

Proper way of Navigation in SwiftUI after async task

I have screen with List of Views one of them is NavigationLink that navigates to separate screen DocumentPreviewView where PDF document is presented.
This PDF document need to be downloaded asynchronously on the first screen after button is tapped and need to be passed to the DocumentPreviewView screen.
I came to working solution but I' m looking for more elegant way and more SwiftUI way.
Maybe document should be some kind of observable object.
This is my current solution. As you can see I use hidden NavigationLink that is being triggered by callback from button action where I also download document which I need to present.
#State private var document = PDFDocument()
#State private var docState: DocState? = .setup
enum DocState: Int {
case setup = 0
case ready = 1
}
var body: some View {
List {
/// some other views
Button(action: {
someAsyncFunction { doc, error in
self.document = doc
self.docState = .ready
}
}) {
Text("Show Document")
}
/// some other views
}
NavigationLink(
destination: DocumentPreviewView(pdfDocument: document),
tag: .ready,
selection: $docState) {
EmptyView()
}
}
Possible solution
struct ContentView: View {
#State private var docState: DocState? = .setup
enum DocState: Int {
case setup = 0
case ready = 1
}
var body: some View {
NavigationView {
ZStack {
NavigationLink(
destination: Text("Preview"),
tag: .ready,
selection: $docState) {
Button(action: {
someTask() { isTaskDone in
docState = .ready
}
}) {
Text("Download")
}
}
}
}
}
func someTask(_ completion: #escaping ((Bool)->())) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(true)
}
}
}

SwiftUI: how to make network request when value of binding changes?

I'm trying to update my chart view when I swipe to another page in PagerView. When I swipe currentIndex changes. But I don't understand how to get notified when currentIndex changes.
Here is my code:
struct MainView: View {
let network = Network()
#State private var currentIndex: Int = 0
#State private var sources: [Source] = []
var body: some View {
return ZStack {
...
VStack {
Text("Температура")
.defaultFont(font: .system(size: 30), weight: .regular)
PagerView(pagesCount: self.sources.count, currentIndex: self.$currentIndex) {
ForEach(self.sources, id: \.self) { t in
...
}
}
if !sources.isEmpty {
ChartView(sourceId: $sources[currentIndex].id)
} else {
Spacer()
}
}
}
}
}
PagerView binds to currentIndex so when I swipe a page currentIndex changes. ChartView has a method loadData and I want to call it when I swipe a page to load new chart depends on sources[currentIndex].id. Here is code of ChartView:
struct ChartView: View {
#Binding var sourceId: String {
didSet {
loadData()
}
}
#State private var points: [TemperaturePoint] = []
var body: some View {
GeometryReader { proxy in
ChartView.makePath(by: self.points, with: proxy)
.stroke(Color.white, lineWidth: 2)
}.onAppear(perform: loadData)
}
func loadData() {
network.getPoints(sourceId: sourceId) { response in
switch response {
case .result(let array):
self.points = TemperaturePoint.smooth(points: array.results)
case .error(let error):
print(error)
}
}
}
}
So question is how to make network calling when Binding variable changes? Or maybe I mistake and need to use another way to code this.
When bound state is changed only body of view with #Binding is called, ie. actually only refresh happens, and only of part dependent of bound state, so even .onAppear is not called.
Taking about into account and the fact that entire ChartView depends on new identifier, the solution would be to make force refresh view as whole, so .onAppear called again, and loaded new data.
Here is code. Tested with Xcode 11.4 / iOS 13.4 (with some simplified replicated version).
struct ChartView: View {
#Binding var sourceId: String // << just binding
#State private var points: [TemperaturePoint] = []
var body: some View {
GeometryReader { proxy in
ChartView.makePath(by: self.points, with: proxy)
.stroke(Color.white, lineWidth: 2)
}
.id(sourceId) // << here !!
.onAppear(perform: loadData)
}
func loadData() {
network.getPoints(sourceId: sourceId) { response in
switch response {
case .result(let array):
self.points = TemperaturePoint.smooth(points: array.results)
case .error(let error):
print(error)
}
}
}
}

FetchedResult views do not update after navigating away from view & back

SwiftUI FetchedResult views fail to update when you navigate away from them and return.
I have a simple todo list app I've created as an example. This app consists of 2 entities:
A TodoList, which can contain many TodoItem(s)
A TodoItem, which belongs to one TodoList
First, here are my core data models:
For the entities, I am using Class Definition in CodeGen.
There are only 4 small views I am using in this example.
TodoListView:
struct TodoListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: TodoList.entity(),
sortDescriptors: []
) var todoLists: FetchedResults<TodoList>
#State var todoListAdd: Bool = false
var body: some View {
NavigationView {
List {
ForEach(todoLists, id: \.self) { todoList in
NavigationLink(destination: TodoItemView(todoList: todoList), label: {
Text(todoList.title ?? "")
})
}
}
.navigationBarTitle("Todo Lists")
.navigationBarItems(trailing:
Button(action: {
self.todoListAdd.toggle()
}, label: {
Text("Add")
})
.sheet(isPresented: $todoListAdd, content: {
TodoListAdd().environment(\.managedObjectContext, self.managedObjectContext)
})
)
}
}
}
This simply fetches all TodoList(s) and spits them out in a list. There is a button in the navigation bar which allows for adding new todo lists.
TodoListAdd:
struct TodoListAdd: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
#State var todoListTitle: String = ""
var body: some View {
NavigationView {
Form {
TextField("Title", text: $todoListTitle)
Button(action: {
self.saveTodoList()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
})
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
})
}
.navigationBarTitle("Add Todo List")
}
.navigationViewStyle(StackNavigationViewStyle())
}
func saveTodoList() {
let todoList = TodoList(context: managedObjectContext)
todoList.title = todoListTitle
do { try managedObjectContext.save() }
catch { print(error) }
}
}
This simply saves a new todo list and then dismisses the modal.
TodoItemView:
struct TodoItemView: View {
#Environment(\.managedObjectContext) var managedObjectContext
var todoList: TodoList
#FetchRequest var todoItems: FetchedResults<TodoItem>
#State var todoItemAdd: Bool = false
init(todoList: TodoList) {
self.todoList = todoList
self._todoItems = FetchRequest(
entity: TodoItem.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "todoList == %#", todoList)
)
}
var body: some View {
List {
ForEach(todoItems, id: \.self) { todoItem in
Button(action: {
self.checkTodoItem(todoItem: todoItem)
}, label: {
HStack {
Image(systemName: todoItem.checked ? "checkmark.circle" : "circle")
Text(todoItem.title ?? "")
}
})
}
}
.navigationBarTitle(todoList.title ?? "")
.navigationBarItems(trailing:
Button(action: {
self.todoItemAdd.toggle()
}, label: {
Text("Add")
})
.sheet(isPresented: $todoItemAdd, content: {
TodoItemAdd(todoList: self.todoList).environment(\.managedObjectContext, self.managedObjectContext)
})
)
}
func checkTodoItem(todoItem: TodoItem) {
todoItem.checked = !todoItem.checked
do { try managedObjectContext.save() }
catch { print(error) }
}
}
This view fetches all of the TodoItem(s) that belong to the TodoList that was tapped. This is where the problem is occurring. I'm not sure if it is because of my use of init() here, but there is a bug. When you first enter this view, you can tap a todo item in order to "check" it and the changes show up in the view immediately. However, when you navigate to a different TodoItemView for a different TodoList and back, the views no longer update when tapped. The checkmark image does not show up, and you need to leave that view and then re-enter it in order for said changes to actually appear.
TodoItemAdd:
struct TodoItemAdd: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
var todoList: TodoList
#State var todoItemTitle: String = ""
var body: some View {
NavigationView {
Form {
TextField("Title", text: $todoItemTitle)
Button(action: {
self.saveTodoItem()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
})
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel")
})
}
.navigationBarTitle("Add Todo Item")
}
.navigationViewStyle(StackNavigationViewStyle())
}
func saveTodoItem() {
let todoItem = TodoItem(context: managedObjectContext)
todoItem.title = todoItemTitle
todoItem.todoList = todoList
do { try managedObjectContext.save() }
catch { print(error) }
}
}
This simply allows the user to add a new todo item.
As I mentioned above, the views stop updating automatically when you leave and re-enter the TodoItemView. Here is a recording of this behaviour:
https://i.imgur.com/q3ceNb1.mp4
What exactly am I doing wrong here? If I'm not supposed to use init() because views in navigation links are initialized before they even appear, then what is the proper implementation?
Found the solution after hours of googling various different phrases of the issue: https://stackoverflow.com/a/58381982/10688806
You must use a "lazy view".
Code:
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
Usage:
NavigationLink(destination: LazyView(TodoItemView(todoList: todoList)), label: {
Text(todoList.title ?? "")
})

Resources