NavigationSplitView inside a TabView looks bad - ios

Migrating to new NavigationSplitView, and the pages are inside a TabView, but it looks weird. How to make it normal like the old one?
NavigationSplitView {
List(users, id: \.self, selection: $selection) { user in
NavigationLink(user, value: user)
}
} detail: {
if let user = selection {
Text(user)
} else {
Text("Pick a node")
}
}

Related

How to make ScrollViewReader scroll to top of List?

I have List within a TabView and allowing the user to scroll to the top when they double tap a tab. I'm using a ScrollViewReader to scroll to a specific anchor. However, it is not fully scrolling to the top of the list because of the navigation title, see the title overlapping the content:
I'm using the technique from this blog post for more context. Below is a working sample:
struct ContentView: View {
#State private var _selectedTab: SelectedTab = .one
#State private var tabbedTwice = false
var selectedTab: Binding<SelectedTab> {
Binding(
get: { _selectedTab },
set: {
if $0 == _selectedTab {
tabbedTwice = true
}
_selectedTab = $0
}
)
}
enum SelectedTab: String {
case one
case two
}
var body: some View {
ScrollViewReader { proxy in
TabView(selection: selectedTab) {
NavigationView {
List {
Section {
ForEach(1...50, id: \.self) { index in
Text("Item \(index.formatted())")
}
}
.id(SelectedTab.one.rawValue)
Section {
Text("Section 2")
}
}
.navigationTitle("First")
}
.tabItem {
Label("One", systemImage: "clock.arrow.circlepath")
}
.tag(SelectedTab.one)
NavigationView {
List {
Section(header: Text("Header").id(SelectedTab.two.rawValue)) {
ForEach(50...100, id: \.self) { index in
Text("Item \(index.formatted())")
}
}
Section {
Text("Section 2")
}
}
.navigationTitle("Second")
}
.tabItem {
Label("Two", systemImage: "list.bullet")
}
.tag(SelectedTab.two)
}
.onChange(of: tabbedTwice) {
guard $0 else { return }
withAnimation { proxy.scrollTo(_selectedTab.rawValue, anchor: .top) }
tabbedTwice = false
}
}
}
}
Is there a better place to put the anchor identifier? I tried putting on the first section and also the section header which worked better but still not scrolling to the very top. How can this be achieved?

The #ObservedResults loads old data in View on deletion, creation or update

Description
I've got simple Combat model which stores name and list of actors. When I delete the Combat from List using onDelete it looks like it's working. It removes the Combat from Realm (checked with RealmStudio) and updates the view. However, if view gets redrawn (for instance, when switching Apps), the "old" data is loaded again (the very first loaded on app initialization), so all deleted rows are back again. Of course, removing them again crashes the app, because they are not present in #ObservedResults combats anymore. Restarting the app fixes the issue, because new data is loaded to #ObservedResults combats and to List, but then again, when I removed something it will be back on review draw...
What I discovered is that removing .sheet() fixes the issue! (EDIT: clarification; it doesn't matter what's inside of the sheet, it may be even empty) The view is updated correctly on redraw! The Sheet is used to display form to add new Combat (nether to say that adding new combats or editing them does not update the view as well, but let's focus on deletion). I have no idea what adding sheet() changes in behaviour of the List and "listening" to #ObservedResults combats.
As a test I used simple array of Combat classes and everything worked. So it points me to issue with #ObservedResults.
I was using the Alert before and all changes to #ObservedResults combats were seen at glance. Now I wanted to replace Alert with Sheet and… That happened.
Also, I have subview where I have almost identical code for actor and there everything works, however I use #ObservedRealmObject var combat: Combat there, and I pass the combat #ObservedResults combats, like so:
NavigationLink(destination: CombatView(combat: combat)) { Text(combat.name) }
I removed unecessary code from below examples to keep it at minimum.
Model
The Combat model:
class Combat: Object, ObjectKeyIdentifiable {
#objc dynamic var id: String = UUID().uuidString
#objc dynamic var name: String = ""
var actors = List<Actor>()
}
Actual View Code (broken using Sheet)
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
struct CombatsListView: View {
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
var body: some View {
List {
ForEach(combats) { combat in
Text(combat.name)
}.onDelete(perform: $combats.remove)
}
.sheet(isPresented: $showAddCombat) {
AddCombatView( showAddCombat: $showAddCombat)
}
}
}
Old View Code (works using Alert)
struct CombatsListView: View {
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
#State private var showAddCombat = false
#State private var addCombatNewName = ""
var body: some View {
List(combats) { combat in
Text(combat.name)
.onDelete(perform: $combats.remove)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAlert = true
}) {
Image(systemName: "plus" )
.font(.title)
Text("New Combat")
}.alert("New Combat", isPresented: $showAlert) {
TextField("write name", text: $addCombatNewName)
Button("Close", role: .cancel) {
addCombatNewName = ""
}
Button("Add") {
addNewCombat(name: addCombatNewName)
addCombatNewName = ""
}
}
}
}
}
private func addNewCombat(name: String) {
let newCombat = Combat()
newCombat.name = name
do {
try self.realm.write {
realm.add(newCombat)
}
} catch {
fatalError("Error: \(error)")
}
}
}
EDITED
I just found some new behaviour. I made a new simple view which lists elements of Collection list and you can delete or add new Collection. It works just fine, but if I include this CollectionsView under the TabView, then the effect is exactly the same as in the example above. The view stops working properly: deleted items are added back on view redraw and adding new objects doesn't refresh the View.
This makes me think more of a bug in #ObservedResults().
Below is the source code.
class Collection: Object, ObjectKeyIdentifiable {
#objc dynamic var id: String = UUID().uuidString
#objc dynamic var name: String = ""
var actors = List<Actor>()
}
#main
struct CombatTrackerApp: App {
var body: some Scene {
WindowGroup {
Tabber() // will not work
// CollectionsView() // will work
}
}
}
struct CollectionsView: View {
#ObservedResults( Collection.self ) var collections
#State private var showNewCollectionForm = false
var body: some View {
NavigationStack {
List {
ForEach(collections) { collection in
Text(collection.name)
}.onDelete(perform: $collections.remove)
}
.listStyle(.inset)
.padding()
.navigationTitle("Collections")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button() {
self.showNewCollectionForm.toggle()
} label: {
Image(systemName: "plus")
Text("Add New Collection")
}
}
}
.sheet(isPresented: $showNewCollectionForm) {
NewCollectionView( showNewCollectionForm: $showNewCollectionForm )
}
}
}
}
struct NewCollectionView: View {
let realm = try! Realm()
#Binding var showNewCollectionForm: Bool
#State private var newCollectioName: String = ""
var body: some View {
NavigationStack {
VStack {
Text("Create new Collection").font(.title).padding()
Form {
TextField("Name", text: $newCollectioName)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Close", role: .cancel) {
showNewCollectionForm.toggle()
}
}
ToolbarItem {
Button("Create") {
addCollection()
} .disabled(newCollectioName.isEmpty)
}
}
}
}
private func addCollection() {
let newCollection = Collection()
newCollection.name = newCollectioName
do {
try realm.write {
realm.add(newCollection)
}
} catch {
print("Cannot add new Collection", error)
}
showNewCollectionForm.toggle()
}
}
struct Tabber: View {
var body: some View {
TabView() {
NavigationStack {
CombatsListView()
}
.tabItem {
Text("Combats")
}
NavigationStack {
CollectionsView()
}
.tabItem {
Text("Collections")
}
SettingsView()
.tabItem {
Text("Settings")
}
}
}
}
I found out the solution (but I still don't understand why it's working).
The solution was to move NavigationStack from my TabView to the subviews. So instead of:
struct Tabber: View {
var body: some View {
TabView() {
NavigationStack {
CombatsListView()
}
.tabItem {
Text("Combats")
}
//...
I should do:
struct Tabber: View {
var body: some View {
TabView() {
CombatsListView()
.tabItem {
Text("Combats")
}
//...
struct CombatsListView: View {
var body: some View {
NavigationStack {
Confusing part was that all online tutorials and Apple Documentation suggests to wrap subviews with NavigationStack in TabView directly instead of adding NavigationStack in subviews. Maybe it's a bug, maybe it's a feature.

SwiftUI Picker and Buttons inside same Form section are triggered by the same user click

I have this AddWorkoutView and I am trying to build some forms similar to what Apple did with "Add new contact" sheet form.
Right now I am trying to add a form more complex than a simple TextField (something similar to "add address" from Apple contacts but I am facing the following issues:
in the Exercises section when pressing on a new created entry (exercise), both Picker and delete Button are triggered at the same time and the Picker gets automatically closed as soon as it gets open and the selected entry is also deleted when going back to AddWorkoutView.
Does anyone have any idea on how Apple implemented this kind of complex form like in the screenshow below?
Thanks to RogerTheShrubber response here I managed to somehow implement at least the add button and to dynamically display all the content I previously added, but I don't know to bring together multiple TextFields/Pickers/any other stuff in the same form.
struct AddWorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#EnvironmentObject var dateModel: DateModel
#Environment(\.presentationMode) var presentationMode
#State var workout: Workout = Workout()
#State var exercises: [Exercise] = [Exercise]()
func getBinding(forIndex index: Int) -> Binding<Exercise> {
return Binding<Exercise>(get: { workout.exercises[index] },
set: { workout.exercises[index] = $0 })
}
var body: some View {
NavigationView {
Form {
Section("Workout") {
TextField("Title", text: $workout.title)
TextField("Description", text: $workout.description)
}
Section("Exercises") {
ForEach(0..<workout.exercises.count, id: \.self) { index in
HStack {
Button(action: { workout.exercises.remove(at: index) }) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.padding(.horizontal)
}
Divider()
VStack {
TextField("Title", text: $workout.exercises[index].title)
Divider()
Picker(selection: getBinding(forIndex: index).type, label: Text("Type")) {
ForEach(ExerciseType.allCases, id: \.self) { value in
Text(value.rawValue)
.tag(value)
}
}
}
}
}
Button {
workout.exercises.append(Exercise())
} label: {
HStack(spacing: 0) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
.padding(.trailing)
Text("add exercise")
}
}
}
}
.navigationTitle("Create new Workout")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
}
.accessibilityLabel("Cancel adding Workout")
}
ToolbarItem(placement: .confirmationAction) {
Button {
} label: {
Text("Done")
}
.accessibilityLabel("Confirm adding the new Workout")
}
}
}
}
}

.onDelete causes a crash because out of index

Does anyone know why this code cause a fatal error: Index out of range, when I try to delete an item from the list? At the moment I am able to create more textfields and populate them but unable to delete anything without the app crashing.
import SwiftUI
struct options: View {
#State var multiOptions = [""]
var body: some View {
VStack {
List {
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
}
.onDelete(perform: removeRow)
}
Button {
multiOptions.append("")
} label: {
Image(systemName: "plus.circle")
}
}
}
func removeRow(at offsets: IndexSet) {
multiOptions.remove(atOffsets: offsets)
}
}
Here is the answer with custom Binding:
struct ContentView: View {
#State var multiOptions = [""]
var body: some View {
VStack {
List {
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: Binding(get: { return multiOptions[index] },
set: { newValue in multiOptions[index] = newValue }))
}
.onDelete(perform: removeRow)
}
Button {
multiOptions.append("")
} label: {
Image(systemName: "plus.circle")
}
}
}
func removeRow(at offsets: IndexSet) {
multiOptions.remove(atOffsets: offsets)
}
}
This seems hard to believe, but apparently the naming of an attribute in the entity as "id" is the cause of this behavior. I changed the name of the UUID attribute to "myID" and the deletions now work. The list view still does not work at all in the preview, but it does now work in the simulator and with a device.

Can not add subtitle to List item inside ForEach in Swift UI

I am trying to make a simple list of cities in Swift UI and I need a List item to contain two rows: title and subtitle.
var body: some View {
NavigationView {
Form {
Section(header: Text("Cities - \(arrayDisplayedCount) results")) {
if self.searchTerm.isEmpty {
List {
ForEach(range, id: \.self) {
VStack {
Text("Header \($0.name)").fontWeight(.bold)
Text("Subheader").fontWeight(.regular)
}
}
}
}
}
}
}
}
This code compiles if I remove the VStack and second Text, in this way it doesn't compile. Please help.
here is the full test code I'm using:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
Form {
Section(header: Text("Cities - some arrayDisplayedCount results")) {
if true {
List {
ForEach(0..<3, id: \.self) { x in
// Text("\($0)") // if you only have this, great no need for "x in"
VStack { // because you have this VStack, you need "x in" and you cannot use $0
Text("\(x)")
Text("Header").fontWeight(.bold)
Text("Subheader").fontWeight(.regular)
}
}
}
}
}
}
}
}
}
try this:
ForEach(range, id: \.self) { _ in
...
}

Resources