Animated NavigationBar coordinated with ScrollView and PaginationView in SwiftUI - swiftui

The Problem
I need to hide and show the navigationBar on SwiftUI where the main content is a PaginationView from SwiftUIX. The below is a modified version of the Asperi's answer ( probably deleted from this site so this is off-site).
The Code
import SwiftUI
import SwiftUIX
struct Home: View {
#State var currentPage = 0
#State private var barHidden = false
var body: some View {
NavigationView {
PaginationView(axis: .horizontal) {
ScrollView(showsIndicators: false) {
VStack {
Text("1\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n2\n3\n4\n5\n6\n7\n8\n9\n8472384723842734 47\n 23473284\n 23847238\n 42384723842384\n 23847238472384\n 23842384\n 2384723847\n 2384234723847\n 234782 34823 423874 2\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n122222 n\n\n\n\n\n\n122222")
.font(.title)
}.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) {
if !barHidden && $0 > 50 {
barHidden = true
print(">> hiding")
} else if barHidden && $0 < 50{
barHidden = false
print("<< showing")
}
}
}
.coordinateSpace(name: "scroll")
.navigationBarTitle("Title Here", displayMode: .inline)
.navigationBarHidden(barHidden)
Text("2")
Text("3")
Text("4")
}.currentPageIndex($currentPage)
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
The question:
Without the PaginationView, the navigation bar is hiding/showing coordinated with up/down scrolls, respectively. It seems that the PaginationView prevents these actions. Is there a solution/workaround to this?
Little search :
Note that there are some other pagers for SwiftUI like
Nacho Navarro' Pages
Fernando Fermoya's SwiftUIPager
These also had the same results and more than that they render all pages at once even every slide movement. I've more than 100 pages so they are not solution, either.
A Youtuber Kavsoft demonstrates some examples without PaginationView, all fails since they use an overlayer to animate, and unfortunately, this prevents the sliding of pages on PaginationView.

Related

SwiftUI .searchable implementation in the wrong way?

I am trying to use a tab bar in order to use different views. On some of those views I have a list of items and I wish that list to be .searchable. If I go to each of the views and search it works like a charm, but when I embed that in the tabbed view the list becomes non-responsive to click but it responds to scroll gesture.
I will expand the idea with code that I have and screenshots, but I am pretty sure that the problem resides in how I'm implementing the combination of the tab bar view and the views that have the searchable modifier:
This code works well
import SwiftUI
struct ClientListView: View {
#ObservedObject var viewModel = ClientFeedViewModel()
#State var searchText: String
#State private var showingSheet = false
#State private var showList = false
var clients: [Client] {
if searchText.count > 2 {
return searchText.isEmpty ? viewModel.clients : viewModel.search(withText: searchText)
}
return viewModel.clients
}
init(){
searchText = ""
}
var body: some View {
NavigationView {
List(clients) { client in
NavigationLink(destination: {
}, label: {
VStack {
Text(client.clientName)
}
})
.listRowSeparator(.hidden)
}
.searchable(text: $searchText)
.listStyle(.plain)
}
}
}
struct ClientListView_Previews: PreviewProvider {
static var previews: some View {
ClientListView()
}
}
The problem starts when I do this and implement the ClientListView in a tab bar view like this:
Tab bar with different views not working searchable modifier
This is the code of the Tab Bar View:
import SwiftUI
struct MainTabView: View {
#EnvironmentObject var viewModel: AuthViewModel
#Binding var selectedIndex: Int
var body: some View {
NavigationView {
VStack {
TabView(selection: $selectedIndex) {
ClientListView()
.onTapGesture {
selectedIndex = 0
}
.tabItem {
Label("Clients", systemImage: "list.bullet")
}.tag(0)
ProjectListView()
.onTapGesture {
selectedIndex = 1
}
.tabItem {
Image(systemName: "person")
Label("Projects", systemImage: "list.dash")
}.tag(1)
TaskListView()
.tabItem {
Image(systemName: "person")
Label("Tasks", systemImage: "list.dash")
}.tag(2)
.onTapGesture {
selectedIndex = 2
}
ClientListView()
.tabItem {
Label("Settings", systemImage: "gear")
}.tag(3)
.onTapGesture {
selectedIndex = 3
}
}
.navigationTitle(tabTitle)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Image("logo_silueta")
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
viewModel.signOut()
}, label: {
Text("logout")
})
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
var tabTitle: String {
switch selectedIndex {
case 0: return "Clients"
case 1: return "Projects"
case 2: return "Tasks"
case 3: return "Settings"
default: return ""
}
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView(selectedIndex: .constant(0))
}
}
Navigation on the tabbed view works and displays the different names on the tab bar title, but when I click cancel or x button of the search bar, it doesn't work and also the list becomes unclickable
So far I haven't been able to find where the problem is but I am assuming its because the tab bar view is messing up with the searchable property
The culprit would seem to be your .onTapGesture modifiers, which will take precedence over any tap handling in your child views.
I'm not sure what value those modifiers bring, since using appropriate .tag values is enough for the tab view to keep track of its selected index. I'd start by removing them.
#ObservedObject var viewModel = ClientFeedViewModel() is a memory leak, try changing it to something like:
struct ClientListViewData {
var searchText: String = ""
var showingSheet = false
var showList = false
mutating func showSheet() {
showingSheet = true
}
}
struct ClientListView: View {
#Binding var data: ClientListViewData

How to enable both wipe able tabview and bottom navigator in swiftUi

I am new with iOS and swiftUi. I got stuck when try to make tabview display as default but also want my tabview is wipeable.
I already know that we can make tabview swipe able by adding tabViewStyle but the navigator in the bottom will be disappeared.
.tabViewStyle(.page(indexDisplayMode: .never))
The solution that I can think of is add a new View in the bottom and custom like a navigator, hope that someone know other better solution than that.
Thank you !
A possible approach is to use two synchronised TabView for this purpose by matching selections and aligned content height. See also comments inline.
Tested with Xcode 13.2 / iOS 15.2
struct TestTwoTabViews: View {
#State private var selection1: Int = 1
#State private var selection2: Int = 1
// to make 2d TabView as height as content view of 1st TabView
#State private var viewHeight = CGFloat.infinity
var body: some View {
ZStack(alignment: .top) {
// responsible for bottom Tabs
TabView(selection: $selection1) {
Color.clear
.background(GeometryReader {
// read 1st content height
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
.tabItem{Image(systemName: "1.square")}.tag(1)
Color.clear
.tabItem{Image(systemName: "2.square")}.tag(2)
Color.clear
.tabItem{Image(systemName: "3.square")}.tag(3)
}
// responsible for paging
TabView(selection: $selection2) {
Color.yellow.overlay(Text("First"))
.tag(1)
Color.green.overlay(Text("Second"))
.tag(2)
Color.blue.overlay(Text("Third"))
.tag(3)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(maxHeight: viewHeight) // content height
}
.onPreferenceChange(ViewHeightKey.self) {
self.viewHeight = $0 // apply content height
}
.onChange(of: selection1) {
selection2 = $0 // sync second
}
.onChange(of: selection2) {
selection1 = $0 // sync first
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = value + nextValue()
}
}

Why SwiftUI-transition does not work as expected when I use it in UIHostingController?

I'm trying to get a nice transition for a view that needs to display date. I give an ID to the view so that SwiftUI knows that it's a new label and animates it with transition. Here's the condensed version without formatters and styling and with long duration for better visualisation:
struct ContentView: View {
#State var date = Date()
var body: some View {
VStack {
Text("\(date.description)")
.id("DateLabel" + date.description)
.transition(.slide)
.animation(.easeInOut(duration: 5))
Button(action: { date.addTimeInterval(24*60*60) }) {
Text("Click")
}
}
}
}
Result, it's working as expected, the old label is animating out and new one is animating in:
But as soon as I wrap it inside UIHostingController:
struct ContentView: View {
#State var date = Date()
var body: some View {
AnyHostingView {
VStack {
Text("\(date.description)")
.id("DateLabel" + date.description)
.transition(.slide)
.animation(.easeInOut(duration: 5))
Button(action: { date.addTimeInterval(24*60*60) }) {
Text("Click")
}
}
}
}
}
struct AnyHostingView<Content: View>: UIViewControllerRepresentable {
typealias UIViewControllerType = UIHostingController<Content>
let content: Content
init(content: () -> Content) {
self.content = content()
}
func makeUIViewController(context: Context) -> UIHostingController<Content> {
let vc = UIHostingController(rootView: content)
return vc
}
func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) {
uiViewController.rootView = content
}
}
Result, the new label is not animated in, rather it's just inserted into it's final position, while the old label is animating out:
I have more complex hosting controller but this demonstrates the issue. Am I doing something wrong with the way I update the hosting controller view, or is this a bug in SwiftUI, or something else?
State do not functioning well between different hosting controllers (it is not clear if this is limitation or bug, just empirical observation).
The solution is embed dependent state inside hosting view. Tested with Xcode 12.1 / iOS 14.1.
struct ContentView: View {
var body: some View {
AnyHostingView {
InternalView()
}
}
}
struct InternalView: View {
#State private var date = Date() // keep relative state inside
var body: some View {
VStack {
Text("\(date.description)")
.id("DateLabel" + date.description)
.transition(.slide)
.animation(.easeInOut(duration: 5))
Button(action: { date.addTimeInterval(24*60*60) }) {
Text("Click")
}
}
}
}
Note: you can also experiment with ObservableObject/ObservedObject based view model - that pattern has different life cycle.

How can I use multiple fullScreenCover in IOS14

I want to present the two destinations view in full screen mode from a single view.
Below is a sample of my code. Seem that the function only works for single presentation, if I have a second fullScreenCover defined, the first fullScreenCover didn't work properly.Is that any workaround at this moment?
import SwiftUI
struct TesFullScreen: View {
init(game : Int){
print(game)
}
var body: some View {
Text("Full Screen")
}
}
ContentView
import SwiftUI
struct ContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
.fullScreenCover(isPresented: self.$showFullScreen1){
TesFullScreen(game: 1)
}
.fullScreenCover(isPresented: self.$showFullScreen2){
TesFullScreen(game: 2)
}
}
}
Not always the accepted answer works (for example if you have a ScrollView with subviews (cells in former days) which holds the buttons, that set the navigational flags).
But I found out, that you also can add the fullScreen-modifier onto an EmptyView. This code worked for me:
// IMPORTANT: Has to be within a container (e.g. VStack, HStack, ZStack, ...)
if myNavigation.flag1 || myNavigation.flag2 {
EmptyView().fullScreenCover(isPresented: $myNavigation.flag1)
{ MailComposer() }
EmptyView().fullScreenCover(isPresented: $myNavigation.flag2)
{ RatingStore() }
}
Usually some same modifier added one after another is ignored. So the simplest fix is to attach them to different views, like
struct FullSContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
.fullScreenCover(isPresented: self.$showFullScreen1){
Text("TesFullScreen(game: 1)")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
.fullScreenCover(isPresented: self.$showFullScreen2){
Text("TesFullScreen(game: 2)")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
}
}
Alternate is to have one .fullScreenCover(item:... modifier and show inside different views depending on input item.
The only thing that worked for me was the answer in this link:
https://forums.swift.org/t/multiple-sheet-view-modifiers-on-the-same-view/35267
Using the EmptyView method or other solutions always broke a transition animation on one of the two presentations. Either transitioning to or from that view and depending on what order I chose them.
Using the approach by Lantua in the link which is using the item argument instead of isPresented worked in all cases:
enum SheetChoice: Hashable, Identifiable {
case a, b
var id: SheetChoice { self }
}
struct ContentView: View {
#State var sheetState: SheetChoice?
var body: some View {
VStack {
...
}
.sheet(item: $sheetState) { item in
if item == .a {
Text("A")
} else {
Text("B")
}
}
}
}
The sheetState needs to be optional for it to work.

swiftUI : Updating Text after changes in settings

[Edit(1) to reflect posting of streamlined app to illustrate the issue : ].
[Edit (2) : completely removed EnvironmentObject and app now works ! Not understanding WHY body is refreshed as NO #State vars are being modified...Code at end of text]
I am writing an app that, at some point, displays some text, related to the contents of 2 Arrays, depending on a set of rules. These rules can be set in a Settings view, as User's preference.
So, when a user changes the rules he wants applied in Settings, that text needs to be re-assessed.
But of course, things aren't that easy.
I present my settings view as modal on my main ContentView, and when I dismiss that modal, the body of the ContentView is not redrawn...
I created an EnvironmentObject with #Published vars in order to keep track of all the user preferences (that are also written to UserDefaults), and shared that #EnvironmentObject with both my ContentView and SettingsView, in the hope that, being an observedObject, its changes would trigger a refresh of my ContentView.
Not so...
Any ideas to help me go forward on this ? Any pointers would be greatly appreciated (again!).
Posted app on GitHub has following architecture :
An appState EnvironmentObject,
A ContentView that displays a set of texts, depending on some user preferences set in
A settingsView
UserDefaults are initialized in AppDelegate.
Thanks for any help on this...
Content view :
import SwiftUI
struct ContentView: View {
#State var modalIsPresented = false // The "settingsView" modally presented as a sheet
#State private var modalViewCaller = 0 // This triggers the appropriate modal (only one in this example)
var body: some View {
NavigationView {
VStack {
Spacer()
VStack {
Text(generateStrings().text1)
.foregroundColor(Color(UIColor.systemGreen))
Text(generateStrings().text2)
} // end of VStack
.frame(maxWidth: .infinity, alignment: .center)
.lineLimit(nil) // allows unlimited lines
.padding(.all)
Spacer()
} // END of main VStack
.onAppear() {
self.modalViewCaller = 0
}
.navigationBarTitle("Test app", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 6 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.sheet(isPresented: $modalIsPresented, content: sheetContent)
.navigationViewStyle(StackNavigationViewStyle()) // This avoids dual column on iPad
} // END of var body: some View
// MARK: #ViewBuilder func sheetContent() :
#ViewBuilder func sheetContent() -> some View {
if modalViewCaller == 6 {
SettingsView()
}
} // END of func sheetContent
// MARK: generateStrings() : -
func generateStrings() -> (text1: String, text2: String, recapText: String, isHappy: Bool) { // minimumNumberOfEventsCheck
var myBool = false
var aString = "" // The text 1 string
var bString = "" // The text 2 string
var cString = "" // The recap string
if UserDefaults.standard.bool(forKey: kmultiRules) { // The user chose the dual rules option
let ruleSet = UserDefaults.standard.integer(forKey: kruleSelection) + 1
aString = "User chose 2 rules option"
bString = "User chose rule set # \(ruleSet)"
myBool = true
print("isDualRules true loop : generateStrings was called at \(Date().debugDescription)")
cString = "Dual rules option, user chose rule set nb \(ruleSet)"
}
else // The user chose the single rule option
{
aString = "User chose single rule option"
bString = "User had no choice : there is only one set of rules !"
myBool = false
print("isDualRules false loop : generateStrings was called at \(Date().debugDescription)")
cString = "Single rule option, user chose nothing."
}
return (aString, bString, cString, myBool)
} // End of func generatestrings() -> String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}
SettingsView :
import SwiftUI
import UIKit
struct SettingsView: View {
#Environment(\.presentationMode) var presentationMode // in order to dismiss the Sheet
#State public var multiRules = UserDefaults.standard.bool(forKey: kmultiRules)
#State private var ruleSelection = UserDefaults.standard.integer(forKey: kruleSelection) // 0 is rule 1, 1 is rule 2
var body: some View {
NavigationView {
List {
Toggle(isOn: $multiRules)
{
Text("more than one rule ?")
}
.padding(.horizontal)
if multiRules {
Picker("", selection: $ruleSelection){
Text("rules 1").tag(0)
Text("rules 2").tag(1)
}.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
}
} // End of List
.navigationBarItems(
leading:
Button("Done") {
self.saveDefaults() // We try to save once more if needed
self.presentationMode.wrappedValue.dismiss() // This dismisses the view
}
)
.navigationBarTitle("Settings", displayMode: .inline)
} // END of Navigation view
} // END of some View
func saveDefaults() {
UserDefaults.standard.set(multiRules, forKey: kmultiRules)
UserDefaults.standard.set(ruleSelection, forKey: kruleSelection)
}
}
// MARK: Preview struct
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
return SettingsView()
}
}
Constants.swift file :
import Foundation
import SwiftUI
let kmultiRules = "two rules"
let kruleSelection = "rules selection"
let kappStateChanged = "appStateChanged"
AppDelegate :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UserDefaults.standard.register(defaults: [ // We initialize the UserDefaults
"two rules": false,
"rules selection": 0, // 0 is ruel 1, 1 is rule 2
"appStateChanged": false
])
return true
}
If you have a shared #EnvironmentObject with #Published properties in two views, if you change such a property from one view, the other one will be re-execute the body property and the view will be updated.
It really helps to create simple standalone examples - not only for asking here, also for gaining a deeper understanding / getting an idea why it doesn't work in the complex case.
For example:
import SwiftUI
class TextSettings: ObservableObject {
#Published var count: Int = 1
}
struct TextSettingsView: View {
#EnvironmentObject var settings: TextSettings
var body: some View {
Form {
Picker(selection: $settings.count, label:
Text("Text Repeat Count"))
{
ForEach(Array(1...5), id: \.self) { value in
Text(String(value)).tag(value)
}
}
}
}
}
struct TextWithSettingExampleView: View {
#EnvironmentObject var settings: TextSettings
var body: some View {
Text(String(repeating: "Hello ", count: Int(settings.count)))
.navigationBarItems(trailing: NavigationLink("Settings", destination: TextSettingsView()))
}
}
struct TextWithSettingExampleView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TextWithSettingExampleView()
}
.environmentObject(TextSettings())
}
}
Not sure I fully understand the question, but I had what I believe might be a similar problem where I never got my contentview to reflect the updates in my observed object when the changes were triggered from a modal. I solved/hacked this by triggering an action in my observed object when dismissing the modal like this:
struct ContentView: View {
//
#State var isPresentingModal = false
var body: some View {
//
.sheet(isPresented: self.$isPresentingModal) {
PresentedModalView()
.onDisappear {
//Do something here
}
}
}
}