SwiftUI list animations looks strange - list

my SwiftUI animations for a list looks strange:
I use nearly the default border plate code from the CoreData starter project in Xcode 12.5:
struct ListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.date, ascending: true)])
private var items: FetchedResults<Item>
#State private var isShowingScanner = false
var body: some View {
NavigationView {
List {
ForEach(items) { item in
Text("Item at \(item.timestamp, formatter: itemFormatter)")
}
.onDelete(perform: deleteItems)
}
.navigationTitle("List")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
EditButton()
// ...
}
}
}
}
private func deleteItems(offsets: IndexSet) {
//...
}
}
Any hint why this is happening?

Related

How to use a Label as a parameter in custom SwiftUI View?

I want to pass a Label to a custom UI component, but I can't figure out the syntax. Here's what I've got:
struct ProOnlyToggle: View {
var label: Label // ERROR: Reference to generic type 'Label' requires arguments in <...>
#Binding var isOn: Bool
var tappedCallback: (() -> (Void))?
#EnvironmentObject var appSettings: AppSettingsModel
var body: some View {
if !appSettings.canUseProFeatures {
Toggle(isOn: $isOn, label: label)
.disabled(true)
.onTapGesture {
tappedCallback?()
}
} else {
Toggle(isOn: $isOn, label: label)
}
}
}
How does one pass a Label into a custom control?
More background:
I'm building a toggle for a feature which is only available to customers who pay. If the user hasn't paid the toggle should be disabled and tapping it should take some action. Here's the kind of thing in vanilla SwiftUI
Toggle(isOn: $isOn) {
Text("Some pro feature")
Text("more info")
.font(.footnote)
}
.disabled(!isPro ? true : false)
.onTapGesture {
if !isPro {
tappedCallback?()
}
}
Because I have quite a few of these switches, I'd like to make a reusable component that could be used like so:
// Note that is pro is passed into the #Environment
ProOnlyToggle(isOn: $settings.someToggle) {
Text("Turn a thing on")
Text("But be aware")
.font(.footnote)
} tappedCallback: {
// Do something in the UI
}
This feels quite simple, but I can't find any examples. What am I missing?
I came up with 3 solutions.
1. Use the generic View type instead of Label
struct ProOnlyToggle<T: View>: View {
init(#ViewBuilder label: #escaping ()->T, isOn: Binding<Bool>) {
self.label = label
self._isOn = isOn
}
let label: ()->T
#Binding var isOn: Bool
var tappedCallback: (() -> (Void))?
var body: some View {
Toggle(isOn: $isOn, label: label)
.disabled(true)
.onTapGesture {
tappedCallback?()
}
}
}
Then, you can use the component like this:
struct TestView: View {
#State private var isON: Bool = false
var body: some View {
ProOnlyToggle(label: { Label(title: { Text("Title Text") },
icon: { Image(systemName: "heart") })},
isOn: $isON)
}
}
2. Use seperate Views for label's title and image
struct ProOnlyToggle<L: View, I: View>: View {
init(#ViewBuilder labelTitle: #escaping ()->L,
#ViewBuilder labelIcon: #escaping ()->I,
isOn: Binding<Bool>) {
self.labelTitle = labelTitle
self.labelIcon = labelIcon
self._isOn = isOn
}
let labelTitle: ()->L
let labelIcon: ()->I
#Binding var isOn: Bool
var tappedCallback: (() -> (Void))?
var body: some View {
Toggle(isOn: $isOn, label: {
Label(title: labelTitle, icon: labelIcon)
})
.disabled(true)
.onTapGesture {
tappedCallback?()
}
}
}
Then you can use the component like this:
struct TestView: View {
#State private var isON: Bool = false
var body: some View {
ProOnlyToggle(labelTitle: {Text("LabelTitle")},
labelIcon: {Image(systemName: "heart")},
isOn: $isON)
}
}
3. Implement and use a custom Label view
struct CustomLabel: View {
let title: String
let systemImageName: String
var body: some View {
Label(title: { Text(title)}, icon: { Image(systemName: systemImageName) } )
}
}
Then, use the CustomLabel as a parameter in the "ProOnlyToggle" view.
struct ProOnlyToggle: View {
let label: CustomLabel
#Binding var isOn: Bool
var tappedCallback: (() -> (Void))?
var body: some View {
Toggle(isOn: $isOn, label: {
label
})
.disabled(true)
.onTapGesture {
tappedCallback?()
}
}
}
And call the ProOnlyToggle anywhere in your app like this:
struct TestView: View {
#State private var isON: Bool = false
var body: some View {
ProOnlyToggle(label: CustomLabel(title: "Title Text",
systemImageName: "heart"),
isOn: $isON)
}
}
At this time, I have not found any better solutions. I hope it may help you.

My environmentObject isn't working.I tap on navigationLink and see nothing in there

My environmentObject isn't working.I tap on navigationLink and see nothing in there.
I change note but it does not get updated.I made viewModel and share data from it everywhere I need it
I made the second TextEditor to do changes to my notes, but I cannot see changes.I just want to write smith and data should be updated
So how can I fix that?
import SwiftUI
#main
struct WhatToDoAppApp: App {
#StateObject private var vm = NoteViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(vm)
}
}
}
//ContentView.swift
import SwiftUI
struct ContentView: View {
#EnvironmentObject var vm: NoteViewModel
#State private var showSheet = false
#State private var searchText = ""
var body: some View {
NavigationView {
List {
ForEach(vm.notes) { item in
NavigationLink(destination: NoteDetailView()) {
Text(item.task)
.lineLimit(1)
}
}
.onDelete(perform: vm.deleteTask)
.onMove(perform: vm.moveTask)
}
.searchable(text: $searchText) {
if !searchResult.isEmpty {
ForEach(searchResult) { item in
NavigationLink(destination: NoteDetailView()) {
Text(item.task)
.lineLimit(1)
}
}
}
}
.navigationBarTitle("Notes")
.safeAreaInset(edge: .bottom) {
Color.clear
.frame(maxHeight: 40)
.background(.gray.opacity(0.7))
HStack {
Spacer(minLength: 160)
Text("\(vm.notes.count) notes")
.foregroundColor(.black.opacity(0.3))
Spacer()
Button {
showSheet = true
} label: {
Image(systemName: "square")
.font(.largeTitle)
.padding(.trailing)
}
}
}
.sheet(isPresented: $showSheet) {
NoteView()
}
}
}
var searchResult: [ToDoItem] {
guard !searchText.isEmpty else { return vm.notes }
return vm.notes.filter { $0.task.contains(searchText) }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.preferredColorScheme(.dark)
ContentView()
.preferredColorScheme(.light)
}
.environmentObject(NoteViewModel())
}
}
//NoteDetailView.swift
import SwiftUI
struct NoteDetailView: View {
#EnvironmentObject var vm: NoteViewModel
var body: some View {
VStack {
TextEditor(text: $vm.text)
Spacer()
}
}
}
struct NotedetailView_Previews: PreviewProvider {
static var previews: some View {
NoteDetailView().environmentObject(NoteViewModel())
}
}
//NoteView.swift
import SwiftUI
struct NoteView: View {
// #State private var text = ""
#EnvironmentObject var vm: NoteViewModel
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
VStack {
TextEditor(text: $vm.text)
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
addTask()
dismiss()
vm.text = ""
}, label: {
Text("Done")
.font(.system(size: 25))
.foregroundColor(.accentColor)
})
}
}
}
}
func addTask() {
vm.add(ToDoItem(task: vm.text))
}
}
struct NoteView_Previews: PreviewProvider {
static var previews: some View {
NoteView()
.environmentObject(NoteViewModel())
}
}
import Foundation
struct ToDoItem: Identifiable, Codable {
var id = UUID()
var task : String
}
class NoteViewModel: ObservableObject {
#Published var notes = [ToDoItem]()
#Published var text = ""
let saveKey = "SavedKey"
init() {
if let data = UserDefaults.standard.data(forKey: saveKey) {
if let decoded = try? JSONDecoder().decode([ToDoItem].self, from: data) {
notes = decoded
return
}
}
notes = []
}
private func save() {
if let encoded = try? JSONEncoder().encode(notes) {
UserDefaults.standard.set(encoded, forKey: saveKey)
}
}
func add(_ note: ToDoItem) {
notes.append(note)
save()
}
func deleteTask(indexSet: IndexSet) {
indexSet.forEach { index in
self.notes.remove(at: index)
save()
}
}
}
The detail view should be a #Binding, and you can use the array that you have in the viewModel as an Bindable List here the fixes:
List {
ForEach($vm.notes) { $item in
NavigationLink(item.task, destination: NoteDetailView(note: $item))
}
The detail view should look like this:
struct NoteDetailView: View {
#Binding var note: ToDoItem
#EnvironmentObject var vm: NoteViewModel
var body: some View {
VStack {
TextEditor(text: $note.task)
Spacer()
}
.onDisappear {
vm.save()
}
}}
This way every time the user updates and closes the modal, the list will be saved.

It does not read that the setting has changed

I have a problem I change the "isDynamic" setting in the "SettingView" and I exit the setting window and the "SongbookView" does not register that the setting has changed. I want to change the search engine depending on what option is selected in the settings. What is the cause of this situation?
SongbookView:
import CoreData
import SwiftUI
struct SongbookView: View {
#State var searchText: String = ""
#State var isSettings: Bool
#ObservedObject var userSettings: UserSettings = UserSettings()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
NavigationView{
VStack{
if userSettings.isDynamic == false {
SearchBar(text: $searchText)
} else {
DynamicSearchBar(text: $searchText)
}
List(songs.filter({searchText.isEmpty ? true : removeNumber(str: $0.content!.lowercased()).contains(searchText.lowercased()) || String($0.number).contains(searchText)}), id:\.objectID) { song in
NavigationLink(destination: DetailView(song: song, isSelected: song.favorite)) {
HStack{
Text("\(String(song.number)). ") .font(.headline) + Text(song.title ?? "Brak tytułu")
if song.favorite {
Spacer()
Image(systemName: "heart.fill")
.accessibility(label: Text("To jest ulubiona pieśń"))
.foregroundColor(.red)
}
}.lineLimit(1)
}
}.id(UUID())
.listStyle(InsetListStyle())
}
.padding(.top, 10)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack{
Text(String(self.userSettings.isDynamic))
Spacer()
Text("Śpiewnik")
.font(.system(size: 20))
.bold()
Spacer()
Button(action: {
isSettings.toggle()
print(userSettings.isDynamic)
}) {
Image(systemName: "gearshape")
.resizable()
.frame(width: 16.0, height: 16.0)
}
.sheet(isPresented: $isSettings) {
SettingView(isPresented: $isSettings)
}
}
}
}
}
}
func removeNumber(str: String) -> String {
var result = str
let vowels: Set<Character> = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
result.removeAll(where: { vowels.contains($0) })
return result
}
}
SettingView:
import SwiftUI
struct SettingView: View {
#ObservedObject var userSettings = UserSettings()
#Binding var isPresented: Bool
var body: some View {
NavigationView {
Form {
Toggle("Dynamiczna wyszukiwarka", isOn: $userSettings.isDynamic)
.onChange(of: userSettings.isDynamic) { value in
print(value)
}
Button(action: {
print(userSettings.isDynamic)
isPresented = false
}) {
Text("Test")
}
}
.navigationBarTitle("Ustawienia")
}
}
}
UserSettings:
import Foundation
import Combine
class UserSettings: ObservableObject {
#Published var isDynamic: Bool {
didSet {
UserDefaults.standard.set(isDynamic, forKey: "isSearchDynamic")
}
}
init() {
self.isDynamic = UserDefaults.standard.object(forKey: "isSearchDynamic") as? Bool ?? false
}
}
You're using two different instances of UserSettings. When you update isDynamic on one of those instances, the other, even though it has a reference to UserDefaults has no reason to know that it needs to update.
The easiest solution here is to share a single instance of UserSettings:
struct SongbookView: View {
#State var searchText: String = ""
#State var isSettings: Bool
#ObservedObject var userSettings: UserSettings = UserSettings()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
//...
.sheet(isPresented: $isSettings) {
SettingView(userSettings: userSettings,isPresented: $isSettings)
}
//...
}
}
struct SettingView: View {
#ObservedObject var userSettings : UserSettings
#Binding var isPresented: Bool
var body: some View {
//...
}
}
You could also look into property wrappers like #AppStorage (https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper) that actually update dynamically when the UserDefaults values change.

SwiftUI NavigationLink in the TitleBar pushes the view twice, a Button doing the same thing is not

This is annoying. The Edit button in the NavigationBar pushes the View twice. I made a test button which behaves correctly doing the same thing:
import SwiftUI
struct DetailListPage: View {
#Environment(\.presentationMode) var presentationMode
var listName: ListNames
// #State private var isEditDetailListPageShowing = false
#State private var selection: String? = nil
var body: some View {
Form {
Section(header: Text(listName.title ?? "")
.font(.title)
.padding()) {
Text(listName.listDetail ?? "Nothing is set yet!")
.multilineTextAlignment(.leading)
.padding()
.cornerRadius(12)
}
NavigationLink(destination: EditDetailListPage(listName: listName)) {
Button {
} label: {
Text("Edit Page")
}
}
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
self.presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
} .padding()
//Edit List Detail
}
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(
destination: EditDetailListPage(listName: listName)) {
Text("Edit")
}
}
}
}
The Text("Edit") right above is pushing the view twice.
The Button above it acts correctly. Would like to use the navigationbaritem instead of the button.
Works well for me, on macos 11.4, xcode 12.5, target ios 14.5 and macCatalyst 11.3.
Probably some other code (or settings/system) that is causing the issue.
What system are you using? Show us the missing code and how you call the views. Let us know if the test code below does not work for you.
This is the test code I used:
#main
struct TestErrorApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct EditDetailListPage: View {
var listName: [String]
var body: some View {
Text("EditDetailListPage")
}
}
struct ContentView: View {
var body: some View {
NavigationView {
DetailListPage(listName: ["test var"])
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailListPage: View {
#Environment(\.presentationMode) var presentationMode
var listName: [String]
// #State private var isEditDetailListPageShowing = false
#State private var selection: String? = nil
var body: some View {
Form {
Section(header: Text("header string")
.font(.title)
.padding()) {
Text("Nothing is set yet!")
.multilineTextAlignment(.leading)
.padding()
.cornerRadius(12)
}
NavigationLink(destination: EditDetailListPage(listName: listName)) {
Button {
} label: {
Text("Edit Page")
}
}
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
self.presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
} .padding()
//Edit List Detail
}
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(
destination: EditDetailListPage(listName: listName)) {
Text("Edit")
}
}
}
}
}
struct ListFrontPage: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
#State private var isAddNewListShowing = false
var listNames: FetchRequest<ListNames>
init() {
listNames = FetchRequest<ListNames>(entity: ListNames.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ListNames.sort, ascending: true)], animation: .default)
}
var body: some View {
VStack {
List() {
Text("Accounts")
.frame(maxWidth: .infinity)
.font(.system(size: 30, weight: .heavy, design: .default))
ForEach (listNames.wrappedValue) { listName in
NavigationLink(destination: DetailListPage(listName: listName)) {
Text("\(listName.title ?? "")")
}
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItem)
}
Spacer()
ZStack {
NavigationLink(destination: AddNewList()) {
Image(systemName: "plus.circle.fill").font(.system(size: 64))
}
}
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button {
self.presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "chevron.backward")
Label("Back", image: "")
}, trailing: EditButton())
.navigationBarTitle(Text("Account Management"))
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { listNames.wrappedValue[$0] }.forEach(CoreDataHelper.sharedManager.deleteItems)
}
}
private func moveItem(from offsets: IndexSet, to destination: Int)
{
print("From: \(String(describing: offsets)) To: \(destination)")
// Make an array of items from fetched results
var revisedItems: [ ListNames ] = listNames.wrappedValue.map{ $0 }
// change the order of the items in the array
revisedItems.move(fromOffsets: offsets, 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 ].sort =
Int16( reverseIndex )
}
}
}

Save selected item in List

This looks like a very simple thing, but I can't figure out how to do this:
I have a List embedded in a NavigationView, containing a NavigationLink to view the detail of the item.
I have a save bar button where I would like to save the selected item. But how can I access the selected item?
It isn't visible in the button's action closure.
struct ItemList : View {
#EnvironmentObject var items: ItemsModel
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(destination: ItemDetail(item: item)) {
Text(item.name)
}
}
.navigationBarTitle(Text("Item"))
.navigationBarItems(trailing: Button(action: {
self.save(/*item: item */) // How can I access item here?
}, label: {
Text("Save")
}))
}
}
func save(item: Item) {
print("Saving...")
}
}
Navigation links are not obligatory to accomplish this.
import SwiftUI
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
var id: Self { self }
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var selectedOceans = [Ocean]()
#State private var multiSelection = Set<Ocean.ID>()
var body: some View {
VStack {
Text("Oceans")
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// Walkaround: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
selectedOceans = Array(multiSelection)
print(selectedOceans)
})
}
Divider()
Text("Selected oceans")
List(selectedOceans, selection: $multiSelection) {
Text($0.name)
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}