Show full screen view overlaying also TabBar - ios

I'm trying to show a view with a loader in full screen. I want also to overlay the TabBar, but I don't know how to do it. Let me show my code.
This is ProgressViewModifier.
// MARK: - View - Extension
extension View {
/// Show a loader binded to `isShowing` parameter.
/// - Parameters:
/// - isShowing: `Bool` value to indicate if the loader is to be shown or not.
/// - text: An optional text to show below the spinning circle.
/// - color: The color of the spinning circle.
/// - Returns: The loader view.
func progressView(
isShowing: Binding <Bool>,
backgroundColor: Color = .black,
dimBackground: Bool = false,
text : String? = nil,
loaderColor : Color = .white,
scale: Float = 1,
blur: Bool = false) -> some View {
self.modifier(ProgressViewModifier(
isShowing: isShowing,
backgroundColor: backgroundColor,
dimBackground: dimBackground,
text: text,
loaderColor: loaderColor,
scale: scale,
blur: blur)
)
}
}
// MARK: - ProgressViewModifier
struct ProgressViewModifier : ViewModifier {
#Binding var isShowing : Bool
var backgroundColor: Color
var dimBackground: Bool
var text : String?
var loaderColor : Color
var scale: Float
var blur: Bool
func body(content: Content) -> some View {
ZStack { content
if isShowing {
withAnimation {
showProgressView()
}
}
}
}
}
// MARK: - Private methods
extension ProgressViewModifier {
private func showProgressView() -> some View {
ZStack {
Rectangle()
.fill(backgroundColor.opacity(0.7))
.ignoresSafeArea()
.background(.ultraThinMaterial)
VStack (spacing : 20) {
if isShowing {
ProgressView()
.tint(loaderColor)
.scaleEffect(CGFloat(scale))
if text != nil {
Text(text!)
.foregroundColor(.black)
.font(.headline)
}
}
}
.background(.clear)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
This is the RootTabView, the one containing the TabBar.
struct RootTabView: View {
var body: some View {
TabView {
AddEverydayExpense()
.tabItem {
Label("First", systemImage: "1.circle")
}
AddInvestment()
.tabItem {
Label("Second", systemImage: "2.circle")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
RootTabView()
}
}
This is my view.
struct AddEverydayExpense: View {
#ObservedObject private var model = AddEverydayExpenseVM()
#State private var description: String = ""
#State private var cost: String = ""
#State private var date: Date = Date()
#State private var essential: Bool = false
#State private var month: Month?
#State private var category: Category?
private var isButtonDisabled: Bool {
return description.isEmpty ||
cost.isEmpty ||
month == nil ||
category == nil
}
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("", text: $description, prompt: Text("Descrizione"))
TextField("", text: $cost, prompt: Text("10€"))
.keyboardType(.numbersAndPunctuation)
DatePicker(date.string(withFormat: "EEEE"), selection: $date)
HStack {
CheckboxView(checked: $essential)
Text("È considerata una spesa essenziale?")
}
.onTapGesture {
essential.toggle()
}
}
Section {
Picker(month?.name ?? "Mese di riferimento", selection: $month) {
ForEach(model.months) { month in
Text(month.name).tag(month as? Month)
}
}
Picker(category?.name ?? "Categoria", selection: $category) {
ForEach(model.categories) { category in
Text(category.name).tag(category as? Category)
}
}
}
Section {
Button("Invia".uppercased()) { print("Button") }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.font(.headline)
.listRowBackground(isButtonDisabled ? Color.gray.opacity(0.5) : Color.blue)
.foregroundColor(Color.white.opacity(isButtonDisabled ? 0.5 : 1))
.disabled(!isButtonDisabled)
}
}
Spacer()
}
.navigationTitle("Aggiungi Spesa")
}
.progressView(isShowing: $model.isFetching, blur: true)
}
}
As you can see, there is the line .progressView(isShowing: $model.isFetching, blur: true) that does the magic. The problem is that the loader is only shown on the current view, but not on the tab. .
How can I achieve the result?

If you want the progress view to cover the entire view (including the tab bar), it has to be in the view hierarchy at or above the TabBar. Right now, it's below the TabBar in the child views.
Because the state will need to be passed up to the parent (the owner of the TabBar), you'll need some sort of state management that you can pass down to the children. This could mean just passing a Binding to a #State. I've chosen to show how to achieve this with an ObservableObject passed down the hierarchy using an #EnvironmentObject so that you don't have to explicitly pass the dependency.
class ProgressManager : ObservableObject {
#Published var inProgress = false
}
struct ContentView : View {
#StateObject private var progressManager = ProgressManager()
var body: some View {
TabView {
AddEverydayExpense()
.tabItem {
Label("First", systemImage: "1.circle")
}
AddInvestment()
.tabItem {
Label("Second", systemImage: "2.circle")
}
}
.environmentObject(progressManager)
.progressView(isShowing: $progressManager.inProgress) //<-- Note that this is outside of the `TabBar`
}
}
struct AddEverydayExpense : View {
#EnvironmentObject private var progressManager : ProgressManager
var body: some View {
Button("Progress") {
progressManager.inProgress = true
}
}
}

Related

SwiftUI custom TabView with paging style

I'm trying to create a custom TabView in SwiftUI, that also has a .tabViewStyle(.page()) functionality too.
At the moment I'm 99% of the way there, but cannot figure out how to get all the TabBarItems to list.
I'm using the PreferenceKey so that the order I add them into the closure is the order in the TabView.
When I run it, the tab items are added to the array, then removed, and it doesn't seem to be working.
I had it working with the enum as CaseIterable and the ForEach(tabs) { tab in as ForEach(TabBarItems.allCases) { tab in, but as mentioned wanted the order in the bar to be organic from the clousure.
Container
struct TabViewContainer<Content : View>: View {
#Binding private var selection: TabBarItem
#State private var tabs: [TabBarItem] = []
var content: Content
init(selection: Binding<TabBarItem>, #ViewBuilder content: () -> Content) {
self._selection = selection
self.content = content()
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
content
}
.tabViewStyle(.page(indexDisplayMode: .never))
tabBarItems()
}
.onPreferenceChange(TabBarItemsPreferenceKey.self) { self.tabs = $0 }
}
private func tabBarItems() -> some View {
HStack(spacing: 10) {
ForEach(tabs) { tab in
Button {
selection = tab
} label: {
tabButton(tab: tab)
}
}
}
.padding(.horizontal)
.frame(maxWidth: .infinity)
.padding(.top, 8)
.background(Color(uiColor: .systemGray6))
}
private func tabButton(tab: TabBarItem) -> some View {
VStack(spacing: 0) {
Image(icon: tab.icon)
.font(.system(size: 16))
.frame(maxWidth: .infinity, minHeight: 28)
Text(tab.title)
.font(.system(size: 10, weight: .medium, design: .rounded))
}
.foregroundColor(selection == tab ? tab.colour : .gray)
}
}
PreferenceKey / Modifier
struct TabBarItemsPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value += nextValue()
}
}
struct TabBarItemViewModifier: ViewModifier {
let tab: TabBarItem
func body(content: Content) -> some View {
content.preference(key: TabBarItemsPreferenceKey.self, value: [tab])
}
}
extension View {
func tabBarItem(_ tab: TabBarItem) -> some View {
modifier(TabBarItemViewModifier(tab: tab))
}
}
Demo view
struct TabSelectionView: View {
#State private var selection: TabBarItem = .itinerary
var body: some View {
TabViewContainer(selection: $selection) {
PhraseView()
.tabBarItem(.phrases)
ItineraryView()
.tabBarItem(.itinerary)
BudgetView()
.tabBarItem(.budget)
BookingView()
.tabBarItem(.bookings)
PackingListView()
.tabBarItem(.packing)
}
}
}
Intended
Current
You can use a more elegant way, #resultBuilder:
You create a struct that holds the View & the tag;
tabBarItem should now return the previously created struct;
The #resultBuilder will then build your array of your view & tag which you'll be using inside the container.
ResultBuilder:
#resultBuilder
public struct TabsBuilder {
internal static func buildBlock(_ components: Tab...) -> [Tab] {
return components
}
internal static func buildEither(first component: Tab) -> Tab {
return component
}
internal static func buildEither(second component: Tab) -> Tab {
return component
}
}
Tab:
struct Tab: Identifiable {
var content: AnyView //I don't recommend the use of AnyView, but I don't want to dive deep into generics for now.
var tag: TabBarItem
var id = UUID()
}
Modifier:
struct Tab: Identifiable {
var content: AnyView
var tag: TabBarItem
var id = UUID()
}
TabViewContainer:
struct TabViewContainer: View {
#Binding private var selection: TabBarItem
#State private var tabs: [TabBarItem]
var content: [Tab]
init(selection: Binding<TabBarItem>, #TabsBuilder content: () -> [Tab]) {
self._selection = selection
self.content = content()
self.tabs = self.content.map({$0.tag})
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
ForEach(content) { content in
content.content
.tag(content.tag)
}
}.tabViewStyle(.page(indexDisplayMode: .never))
tabBarItems()
}
}
private func tabBarItems() -> some View {
HStack(spacing: 10) {
ForEach(tabs) { tab in
Button {
selection = tab
} label: {
tabButton(tab: tab)
}
}
}
.padding(.horizontal)
.frame(maxWidth: .infinity)
.padding(.top, 8)
.background(Color(uiColor: .systemGray6))
}
private func tabButton(tab: TabBarItem) -> some View {
VStack(spacing: 0) {
Image(icon: tab.icon)
.font(.system(size: 16))
.frame(maxWidth: .infinity, minHeight: 28)
Text(tab.title)
.font(.system(size: 10, weight: .medium, design: .rounded))
}
.foregroundColor(selection == tab ? tab.colour : .gray)
}
}

How to scroll through items in scroll view using keyboard arrows in SwiftUI?

I've built a view that has scroll view of horizontal type with HStack for macOS app. Is there a way to circle those items using keyboard arrows?
(I see that ListView has a default behavior but for other custom view types there are none)
click here to see the screenshot
var body: some View {
VStack {
ScrollView(.horizontal, {
HStack {
ForEach(items.indices, id: \.self) { index in
//custom view for default state and highlighted state
}
}
}
}
}
any help is appreciated :)
You could try this example code, using my previous post approach, but with a horizontal scrollview instead of a list. You will have to adjust the code to your particular app. My approach consists only of a few lines of code that monitors the key events.
import Foundation
import SwiftUI
import AppKit
struct ContentView: View {
let fruits = ["apples", "pears", "bananas", "apricot", "oranges"]
#State var selection: Int = 0
#State var keyMonitor: Any?
var body: some View {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 0) {
ForEach(fruits.indices, id: \.self) { index in
VStack {
Image(systemName: "globe")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(10)
Text(fruits[index]).tag(index)
}
.background(selection == index ? Color.red : Color.clear)
.padding(10)
}
}
}
.onAppear {
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { nsevent in
if nsevent.keyCode == 124 { // arrow right
selection = selection < fruits.count ? selection + 1 : 0
} else {
if nsevent.keyCode == 123 { // arrow left
selection = selection > 1 ? selection - 1 : 0
}
}
return nsevent
}
}
.onDisappear {
if keyMonitor != nil {
NSEvent.removeMonitor(keyMonitor!)
keyMonitor = nil
}
}
}
}
Approach I used
Uses keyboard shortcuts on a button
Alternate approach
To use commands (How to detect keyboard events in SwiftUI on macOS?)
Code:
Model
struct Item: Identifiable {
var id: Int
var name: String
}
class Model: ObservableObject {
#Published var items = (0..<100).map { Item(id: $0, name: "Item \($0)")}
}
Content
struct ContentView: View {
#StateObject private var model = Model()
#State private var selectedItemID: Int?
var body: some View {
VStack {
Button("move right") {
moveRight()
}
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
ScrollView(.horizontal) {
LazyHGrid(rows: [GridItem(.fixed(180))]) {
ForEach(model.items) { item in
ItemCell(
item: item,
isSelected: item.id == selectedItemID
)
.onTapGesture {
selectedItemID = item.id
}
}
}
}
}
}
private func moveRight() {
if let selectedItemID {
if selectedItemID + 1 >= model.items.count {
self.selectedItemID = model.items.last?.id
} else {
self.selectedItemID = selectedItemID + 1
}
} else {
selectedItemID = model.items.first?.id
}
}
}
Cell
struct ItemCell: View {
let item: Item
let isSelected: Bool
var body: some View {
ZStack {
Rectangle()
.foregroundColor(isSelected ? .yellow : .blue)
Text(item.name)
}
}
}

SwiftUI view over all the views including sheet view

I need to show a view above all views based upon certain conditions, no matter what the top view is. I am trying the following code:
struct TestView<Presenting>: View where Presenting: View {
/// The binding that decides the appropriate drawing in the body.
#Binding var isShowing: Bool
/// The view that will be "presenting" this notification
let presenting: () -> Presenting
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .top) {
self.presenting()
HStack(alignment: .center) {
Text("Test")
}
.frame(width: geometry.size.width - 44,
height: 58)
.background(Color.gray.opacity(0.7))
.foregroundColor(Color.white)
.cornerRadius(20)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
extension View {
func showTopView(isShowing: Binding<Bool>) -> some View {
TestView(isShowing: isShowing,
presenting: { self })
}
}
struct ContentView: View {
#State var showTopView = false
NavigationView {
ZStack {
content
}
}
.showTopView(isShowing: $showTopView)
}
Now this is working fine in case of the views being pushed. But I am not able to show this TopView above the presented view.
Any help is appreciated!
Here is a way for your goal, you do not need Binding, just use let value.
struct ContentView: View {
#State private var isShowing: Bool = Bool()
var body: some View {
CustomView(isShowing: isShowing, content: { yourContent }, isShowingContent: { isShowingContent })
}
var yourContent: some View {
NavigationView {
VStack(spacing: 20.0) {
Text("Hello, World!")
Button("Show isShowing Content") { isShowing = true }
}
.navigationTitle("My App")
}
}
var isShowingContent: some View {
ZStack {
Color.black.opacity(0.5).ignoresSafeArea()
VStack {
Spacer()
Button("Close isShowing Content") { isShowing = false }
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue.cornerRadius(10.0))
.padding()
}
}
}
}
struct CustomView<Content: View, IsShowingContent: View>: View {
let isShowing: Bool
#ViewBuilder let content: () -> Content
#ViewBuilder let isShowingContent: () -> IsShowingContent
var body: some View {
Group {
if isShowing { ZStack { content().blur(radius: isShowing ? 5.0 : 0.0); isShowingContent() } }
else { content() }
}
.animation(.default, value: isShowing)
}
}

SwiftUI - Animate View expansion (show / hide)

I have a View that contains a HStack and a DatePicker. When you tap on the HStack, the DatePicker is shown / hidden. I want to animate this action like the animation of Starts and Ends row in iOS Calendar's New Event View.
struct TimePicker: View {
#Binding var startTime: Date
#State private var isDatePickerVisible: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack {
ListItemView(icon: "start-time",
leadingText: "Start Time",
trailingText: startTime.stringTime())
}
.onTapGesture {
withAnimation {
self.isDatePickerVisible.toggle()
}
}
Group {
if isDatePickerVisible {
DatePicker("", selection: $startTime, displayedComponents: [.hourAndMinute])
.datePickerStyle(WheelDatePickerStyle())
}
}
.background(Color.red)
.modifier(AnimatingCellHeight(height: isDatePickerVisible ? 300 : 0))
}
}
}
I have used the following code for animation. It almost works. The only problem is that HStack jumps. And I can not fix it.
https://stackoverflow.com/a/60873883/8292178
struct AnimatingCellHeight: AnimatableModifier {
var height: CGFloat = 0
var animatableData: CGFloat {
get { height }
set { height = newValue }
}
func body(content: Content) -> some View {
content.frame(height: height)
}
}
How to fix this issue? How to animate visibility of the DatePicker?
It's simple, you don't need extra ViewModifier
struct TimePicker: View {
#Binding var startTime: Date
#State private var isDatePickerVisible: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack {
ListItemView(icon: "start-time"
, leadingText: "Start Time"
, trailingText: startTime.stringTime())
}.onTapGesture {
isDatePickerVisible.toggle()
}
if isDatePickerVisible {
DatePicker(""
, selection: $model.startTime
, displayedComponents: [.hourAndMinute]
).datePickerStyle(WheelDatePickerStyle())
}
}.animation(.spring())
}
}

Present a new view in SwiftUI

I want to click a button and then present a new view like present modally in UIKit
I have already seen "How to present a new view using sheets", but I don't want to attach it to the main view as a modal sheet.
And I don't want to use NavigationLink, because I don't want a new view and old view have a navigation relationship.
Thanks for your help...
To show a modal (iOS 13 style)
You just need a simple sheet with the ability to dismiss itself:
struct ModalView: View {
#Binding var presentedAsModal: Bool
var body: some View {
Button("dismiss") { self.presentedAsModal = false }
}
}
And present it like:
struct ContentView: View {
#State var presentingModal = false
var body: some View {
Button("Present") { self.presentingModal = true }
.sheet(isPresented: $presentingModal) { ModalView(presentedAsModal: self.$presentingModal) }
}
}
Note that I passed the presentingModal to the modal so you can dismiss it from the modal itself, but you can get rid of it.
To make it REALLY present fullscreen (Not just visually)
You need to access to the ViewController. So you need some helper containers and environment stuff:
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController)
}
}
extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
Then you should use implement this extension:
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, #ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: "dismissModal"), object: nil, queue: nil) { [weak toPresent] _ in
toPresent?.dismiss(animated: true, completion: nil)
}
self.present(toPresent, animated: true, completion: nil)
}
}
Finally
you can make it fullscreen like:
struct ContentView: View {
#Environment(\.viewController) private var viewControllerHolder: UIViewController?
var body: some View {
Button("Login") {
self.viewControllerHolder?.present(style: .fullScreen) {
Text("Main") // Or any other view you like
// uncomment and add the below button for dismissing the modal
// Button("Cancel") {
// NotificationCenter.default.post(name: Notification.Name(rawValue: "dismissModal"), object: nil)
// }
}
}
}
}
For iOS 14 and Xcode 12:
struct ContentView: View {
#State private var isPresented = false
var body: some View {
Button("Show Modal with full screen") {
self.isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}
}
struct FullScreenModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("This is a modal view")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
See also: How to present a full screen modal view using fullScreenCover()
Disclaimer: Below is not really like a "native modal", neither behave nor look&feel, but if anyone would need a custom transition of one view over other, making active only top one, the following approach might be helpful.
So, if you expect something like the following
Here is a simple code for demo the approach (of corse animation & transition parameters can be changed by wish)
struct ModalView : View {
#Binding var activeModal: Bool
var body : some View {
VStack {
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.activeModal = false
}
}) {
Text("Hide modal")
}
Text("Modal View")
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.green)
}
}
struct MainView : View {
#Binding var activeModal: Bool
var body : some View {
VStack {
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.activeModal = true
}
}) {
Text("Show modal")
}
Text("Main View")
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.yellow)
}
}
struct ModalContainer: View {
#State var showingModal = false
var body: some View {
ZStack {
MainView(activeModal: $showingModal)
.allowsHitTesting(!showingModal)
.disabled(showingModal)
if showingModal {
ModalView(activeModal: $showingModal)
.transition(.move(edge: .bottom))
.zIndex(1)
}
}
}
}
Here is a simple one way - forward views. It's very straight forward.
struct ChildView: View{
private let colors: [Color] = [.red, .yellow,.green,.white]
#Binding var index : Int
var body: some View {
let next = (self.index+1) % MyContainer.totalChildren
return ZStack{
colors[self.index % colors.count]
Button("myNextView \(next) ", action: {
withAnimation{
self.index = next
}
}
)}.transition(.asymmetric(insertion: .move(edge: .trailing) , removal: .move(edge: .leading) ))
}
}
struct MyContainer: View {
static var totalChildren = 10
#State private var value: Int = 0
var body: some View {
HStack{
ForEach(0..<(Self.totalChildren) ) { index in
Group{
if index == self.value {
ChildView(index: self.$value)
}}
}
}
}
}
then presents it from ContentView when a button is tapped:
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
Button("Press to dismiss") {
dismiss()
}
.font(.title)
.padding()
.background(Color.black)
}
}
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
Button("Show Sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
}
}

Resources