SwiftUI how to show large navigation title without using a navigation view? - ios

I'm using the below PullToRefreshHack from this SO Answer which works well, but the problem is when I wrap it inside a NavigationView (which I need to be able to show this view's large navigation title) I loose the functionality of the pull to refresh. I'm not sure why though? 🤔 How can I fix this, or remove the NavigationView but still show a large title at the top?
//Usage
var body: some View {
NavigationView {
ScrollView {
PullToRefreshHack(coordinateSpaceName: "pullToRefreshInTrendsView") {
print("user pulled to refresh")
generator.impactOccurred()
self.loadDataForTrendsView()
}
struct PullToRefreshHack: View {
var coordinateSpaceName: String
var onRefresh: ()->Void
#State var needRefresh: Bool = false
var body: some View {
GeometryReader { geo in
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
Spacer()
.onAppear {
needRefresh = true
}
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
Spacer()
.onAppear {
if needRefresh {
needRefresh = false
onRefresh()
}
}
}
HStack {
Spacer()
if needRefresh {
ProgressView()
} else {
Text("")
}
Spacer()
}
.onAppear {
//print("PullToRefreshHack VIEW .onAppear is called")
}
}.padding(.top, -50)
}
}

Related

Why does withAnimation not cause an animation?

I try to switch from one screen to another by pressing a button (see full code below). The switch from the first view to the second view (and vice versa) works, but no animation is taking place.
Why does this behavior happen?
Full code:
struct ContentView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
var body: some View {
if isFirstViewActive {
FirstView()
} else {
SecondView()
}
}
}
struct FirstView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
var body: some View {
ZStack {
Color(.red).ignoresSafeArea(.all, edges: .all)
VStack {
Spacer()
Text("This is the first view")
Spacer()
Button {
withAnimation {
isFirstViewActive = false
}
} label: {
Text("Go to second view")
}
Spacer()
}
}
}
}
struct SecondView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = false
var body: some View {
ZStack {
Color(.blue).ignoresSafeArea(.all, edges: .all)
VStack {
Spacer()
Text("This is the second view")
Spacer()
Button {
withAnimation {
isFirstViewActive = true
}
} label: {
Text("Go to first view")
}
Spacer()
}
}
}
}
The problem is with
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
If you change that to
#State var isFirstViewActive: Bool = true
and use #Binding in subviews, you will get the default animations.
In iOS 16, there seems to be a problem with #AppStorage vars and animation. But you can refer to this workaround

SwiftUI LazyVStack with pinned views

Context: I need to pin a view in a ScrollView to the top of the screen when scrolling, so I use a LazyVStack with pinnedViews, set the view I need as Section. All good.
Issue: The ScrollView other views might change the content while the view is scrolled to the bottom, when that happens the screen removes all views and doesn't display them back unless I scroll to the top.
Question: Is there another way to pin a view to the top? (I tried to use List, but not exactly what I need) Or is possible to make a custom Stack with pinned views?
This is my answer for another post, but it seems like you both are having a similar problem. Download and import TrackableScrollView, and try out the below code. While scrolling, there is a pinned View() which is displayed at the top of the screen.
Link package: https://github.com/maxnatchanon/trackable-scroll-view
Code:
import SwiftUI
import SwiftUITrackableScrollView //Added
import Combine
struct GameTabView: View {
#State private var scrollViewContentOffset = CGFloat(0) //Added
#State var selectedTab: Int = 0
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
ZStack { //Added
TrackableScrollView(.vertical, showIndicators: true, contentOffset: $scrollViewContentOffset) {
VStack {
Color.gray.frame(height: 400)
sectionView
}
}
if(scrollViewContentOffset > 400) {
VStack {
headerView
Spacer()
}
}
}
}
var sectionView: some View {
Section {
tabContentView
.transition(.scale) // FIXED
.background(Color.blue)
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
switch selectedTab {
case 0:
DummyScreen(title: "FIRST", color: .red)
case 1:
DummyScreen(title: "SECOND", color: .green)
case 2:
DummyScreen(title: "THIRD", color: .blue)
default:
EmptyView()
}
}
}
struct DummyScreen: View {
let title: String
let color: Color
var body: some View {
VStack {
ForEach(0..<15, id: \.self) { index in
HStack {
Text("#\(index): title \(title)")
.foregroundColor(Color.black)
.font(.system(size: 30))
.padding(.vertical, 20)
Spacer()
}
.background(Color.yellow)
}
}
.background(color)
}
}
From personal experience, I have done this using a VStack.
VStack{
PinnedItems()
LazyVStack {
OtherItems()
}
}
Then Pinned items will not scroll and are permanently attached to the top. And the lazyVStack will still be able to scroll underneath.

Scroll view confuses size of content between tabs

I have a scroll view with two buttons that switch the content of the scroll view. Content for the first tab is very big and for the second tab its very small compared to the first.
The bug is not very consistent and it is hard to reproduce, but the best way to reproduce it is to scroll while the tab with the large content is selected, then stop the scroll midway with a tap and switch to the other tab. And then only the background is shown no items are loaded until you scroll a bit and the scroll view fixes its content size. When the bug happens the scroll indicator stays very small even tho we have moved to the tab that does not have so much content and its usual scroll indicator is much bigger
Edit: I have now tried using .id(UUID()) based on https://www.youtube.com/watch?v=h0SgafWwoh8, and also I tried using ScrollViewReader to scroll to the top of each section when content changes (with .onChange(of content)). Both those did not work.
struct ContentView: View {
enum Content: String {
case one
case two
}
#State private var contentSelection: Content = .one
var body: some View {
ZStack {
Color.red
GeometryReader { proxy in
ScrollView {
ZStack {
LazyVStack(pinnedViews: .sectionHeaders) {
Text("There is some other content here")
.frame(height: proxy.size.height/2)
.background(Color.orange)
Section(header:
HStack {
Spacer()
Button(action: { contentSelection = .one }) {
Text("One")
.foregroundColor(contentSelection == .one ? .black : .gray)
}
Button(action: { contentSelection = .two }) {
Text("two")
.foregroundColor(contentSelection == .two ? .black : .gray)
}
Spacer()
}
.padding()
.background(Color.yellow)
) {
switch contentSelection {
case .one:
LazyVStack {
ForEach((1...500), id: \.self) { item in
ZStack {
Text("This is item number \(item)")
}
.frame(height: 80)
}
}
case .two:
LazyVStack {
ForEach((1...50), id: \.self) { item in
ZStack {
Text("This is item number \(item)")
}
.frame(height: 80)
}
}
}
}
}
}
.background(Color.green)
}
}
}
}
}
struct ContentView: View {
enum Content: String {
case one
case two
}
#State private var contentSelection: Content = .one
var body: some View {
ZStack {
Color.red
GeometryReader { proxy in
ScrollView {
ZStack {
LazyVStack(pinnedViews: .sectionHeaders) {
Text("There is some other content here")
.frame(height: proxy.size.height/2)
.background(Color.orange)
Section(header: headerView
) {
switch contentSelection {
case .one:
content1
case .two:
content2
}
}
}
}
.background(Color.green)
}
}
}
}
var content1: some View {
LazyVStack {
ForEach((1...500), id: \.self) { item in
ZStack {
Text("This is item number \(item)")
}
.frame(height: 80)
}
}
}
var content2: some View {
LazyVStack {
ForEach((1...500), id: \.self) { item in
ZStack {
Text("This is item number \(item)")
}
.frame(height: 80)
}
}
}
var headerView: some View {
HStack {
Spacer()
Button(action: { contentSelection = .one }) {
Text("One")
.foregroundColor(contentSelection == .one ? .black : .gray)
}
Button(action: { contentSelection = .two }) {
Text("two")
.foregroundColor(contentSelection == .two ? .black : .gray)
}
Spacer()
}
.padding()
.background(Color.yellow)
}
}

Navigation View -Notes Like Sidebar - Toggle button

I am very new to iOS development and Swift UI. I am making an app for our company. I believe Apple Notes like approach is best. I got most working tanks to some Udemmy courses and a couple of weeks of intense Googling. But I can't figure out how to implement the toggle sidebar button. I am probably searching for something obvious but using the wrong terminology.
I am talking about this:
When I remove most of the code, I have a structure like this:
NavigationView {
List {
Section(header: RoomHeader()) {
ForEach(sections) { section in
NavigationLink(destination: ViewRoom(section: section)) {
RoomListItem(section: section)
}
}
}
}
.navigationTitle("Rooms")
.listStyle(InsetGroupedListStyle())
}
The ViewRoom class
import SwiftUI
struct ViewRoom: View {
var room: RoomModel
var body: some View {
ZStack {
ScrollView {
VStack {
controls
title
// ....
}
.padding()
}
bottomBar
}
.navigationTitle(room.name)
.navigationBarItems(
trailing: HStack {
// ...
}
)
}
var controls: some View {
HStack {
Spacer()
// Couldn't find the icon on SF Symbols but this is the toggle button
Button(action: {}, label: {
Image(systemName: "rectangle.portrait.arrowtriangle.2.outward")
})
}
.font(.system(size: 24))
.padding(.top, 15)
}
// ...
}
I'd appreciate it if you could let me know if how to implement this toggle feature.
You can use Zstack & animation & transition features of SwiftUI. I made a sample for you to dig more into it and explore more about above mentioned concepts.
struct ContentView: View {
#State private var showDetails = false
var body: some View {
ZStack {
if showDetails {
LeftView()
.transition(.move(edge: .leading))
.zIndex(1)
} else {
Button("Press to show details") {
withAnimation(.spring()) {
showDetails.toggle()
}
}
}
}
.onTapGesture {
withAnimation(.spring()) {
showDetails.toggle()
}
}
}
}
Below is leftView which will animated from left
struct LeftView: View {
var body: some View {
GeometryReader { geometry in
VStack {
Text("Hello, World!")
}
.frame(width: 200, height: geometry.size.height)
.background(Color.blue)
}
}
}

Show different views from NavigationBarItem menu in SwiftUI

I am trying to show a different view based on the option chosen in the NavigationBar menu. I am getting stuck on the best way to do this.
First, based on my current approach (I think it is not right!), I get a message in Xcode debugger when I press the menu item:
SideMenu[16587:1131441] [UILog] Called -[UIContextMenuInteraction
updateVisibleMenuWithBlock:] while no context menu is visible. This
won't do anything.
How do I fix this?
Second, When I select an option from the menu, how do I reset the bool so that it does not get executed unless it is chosen from the menu again. Trying to reset as self.showNewView = false within the if condition gives a compiler error
Here is a full executable sample code I am trying to work with. Appreciate any help in resolving this. Thank you!
struct ContentView: View {
#State var showNewView = false
#State var showAddView = false
#State var showEditView = false
#State var showDeleteView = false
var body: some View {
NavigationView {
GeometryReader { g in
VStack {
if self.showAddView {
AddView()
}
if self.showNewView {
NewView()
}
if self.showEditView {
EditView()
}
if self.showDeleteView {
DeleteView()
}
}.frame(width: g.size.width, height: g.size.height)
}
.navigationTitle("Title")
.navigationBarItems(leading: {
Menu {
Button(action: {showNewView.toggle()}) {
Label("New", systemImage: "pencil")
}
Button(action: {showEditView.toggle()}) {
Label("Edit", systemImage: "square.and.pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}(), trailing: {
Menu {
Button(action: {showAddView.toggle()}) {
Label("Add", systemImage: "plus")
}
Button(action: {showDeleteView.toggle()}) {
Label("Delete", systemImage: "trash")
}
} label: {
Image(systemName: "plus")
}
}())
}
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct NewView: View {
var body: some View {
GeometryReader { g in
Text("This is New View")
}
.background(Color.red)
}
}
struct EditView: View {
var body: some View {
GeometryReader { g in
Text("This is Edit View")
}
.background(Color.green)
}
}
struct AddView: View {
var body: some View {
GeometryReader { g in
Text("This is Add View")
}
.background(Color.orange)
}
}
struct DeleteView: View {
var body: some View {
GeometryReader { g in
Text("This is Delete View")
}
.background(Color.purple)
}
}
Here is what I get when I select each of the menu items. I would like to be able to show only one menu item. Essentially dismiss the other one when a new menu item is selected
A possible solution is to use a dedicated enum for your current view (instead of four #State properties):
enum CurrentView {
case new, add, edit, delete
}
#State var currentView: CurrentView?
Note that you can also extract parts of code to computed properties.
Here is a full code:
enum CurrentView {
case new, add, edit, delete
}
struct ContentView: View {
#State var currentView: CurrentView?
var body: some View {
NavigationView {
GeometryReader { g in
content
.frame(width: g.size.width, height: g.size.height)
}
.navigationTitle("Title")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: leadingBarItems, trailing: trailingBarItems)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var content: some View {
if let currentView = currentView {
switch currentView {
case .add:
AddView()
case .new:
NewView()
case .edit:
EditView()
case .delete:
DeleteView()
}
}
}
var leadingBarItems: some View {
Menu {
Button(action: { currentView = .new }) {
Label("New", systemImage: "pencil")
}
Button(action: { currentView = .edit }) {
Label("Edit", systemImage: "square.and.pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
var trailingBarItems: some View {
Menu {
Button(action: { currentView = .add }) {
Label("Add", systemImage: "plus")
}
Button(action: { currentView = .delete }) {
Label("Delete", systemImage: "trash")
}
} label: {
Image(systemName: "plus")
}
}
}

Resources