Frame transition animation works conditionally - ios

I am trying to make child view which contains string array moves back and forth with animation as button on parent view is toggled.
However, child view just show up and disappeared without any animation at all.
struct ParentView: View {
#State isToggle: Bool
var body: some View {
ChildView(isToggle: isToggle)
.onTabGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
let values = [one, two, three, four]
var isToggle: Bool
var body: some View {
HStack {
ForEach(values) { value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
I changed code(stored property to viewmodel) as below. and It works as expected.
class ViewModel: ObservableObject {
#Published var values = [one, two, three, four]
}
struct ParentView: View {
#State isToggle: Bool
var body: some View {
ChildView(isToggle: isToggle)
.onTabGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
#EnvironmentObject private vm: ViewModel
var isToggle: Bool
var body: some View {
HStack {
ForEach(values) { vm.value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
I thought that toggling state redraws view only when with stored property.
But, child view with viewmodel is still redrawn when toggle state changes.
Data itself not changed at all. Please kindly let me know why this is happening.

there are some minor typos in your first code. If I correct them the code runs, and the animation works:
struct ContentView: View {
#State private var isToggle: Bool = false
var body: some View {
ChildView(isToggle: isToggle)
.onTapGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
let values = ["one", "two", "three", "four"]
var isToggle: Bool
var body: some View {
HStack {
ForEach(values, id: \.self) { value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
}

Related

Conditional component in SwiftUI

I'm giving my first steps with SwiftUI and I'm having problems with a component shown depending on a condition.
I'm trying to show a fullscreen popup (full screen with semi transparent black background and the popup in the middle with white background). To achieve this I've made this component:
struct CustomUiPopup: View {
var body: some View {
ZStack {
}
.overlay(CustomUiPopupOverlay, alignment: .top)
.zIndex(1)
}
private var CustomUiPopupOverlay: some View {
ZStack {
Spacer()
ZStack {
Text("POPUP")
.padding()
}
.zIndex(1)
.frame(width: UIScreen.main.bounds.size.width - 66)
.background(Color.white)
.cornerRadius(8)
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
.background(Color.black.opacity(0.6))
}
}
If I set this in my main view, the popup is shown correctly over the button:
struct MainView: View {
var body: some View {
CustomUiPopup()
Button("Click to show popup") {
print("click on button")
}
}
}
If I set this, my popup is not shown (correct because hasToShowPopup is false), but if I click on the button it fails, the popup is not shown and the button can not be clicked again (?!), it seems like the view was freezed.
struct MainView: View {
#State private var hasToShowPopup = false
var body: some View {
if hasToShowPopup {
CustomUiPopup()
}
Button("Click to show popup") {
hasToShowPopup = true
}
}
}
I've even tried to initializate hasToShowPopup to true but the popup keeps failing, it's not shown in the first place:
struct MainView: View {
#State private var hasToShowPopup = true
var body: some View {
if hasToShowPopup {
CustomUiPopup()
}
Button("Click to show popup") {
hasToShowPopup = true
}
}
}
So my conclusion is that, I don't know why, but if I put my CustomUiPopup inside an "if" something is not rendered correctly.
What is wrong with my code?
Anyway, if this is not the correct approach to show a popup, I'll be glad to have any advice.
Following Ptit Xav suggestion I've tried this with the same results (my CustomUiPopup doesn't show):
struct MainView: View {
#State private var hasToShowPopup = false
var body: some View {
VStack {
if hasToShowPopup {
CustomUiPopup()
}
Button("Click to show popup") {
hasToShowPopup = true
}
}
}
}
This works fine with me:
struct CustomUiPopup: View {
var body: some View {
ZStack {
Spacer()
Text("POPUP")
.padding()
.zIndex(1)
.frame(width: UIScreen.main.bounds.size.width - 66)
.background(Color.white)
.cornerRadius(8)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
Color.black.opacity(0.6)
.ignoresSafeArea()
)
}
}
struct ContentView: View {
#State private var hasToShowPopup = false
var body: some View {
ZStack {
Button("Click to show popup") {
hasToShowPopup = true
}
if hasToShowPopup {
CustomUiPopup()
}
}
}
}

Show full screen view overlaying also TabBar

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
}
}
}

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)
}
}

How to change the label of a button by using an if statement in SwiftUI

I want my button to look different after pressing the button. I am trying to do so by using a ZStack and an if statement. I don't understand why it won't toggle between a black button and a white button... P.S. I am getting experience using ObservableObject protocol.
class WelcomeButton: ObservableObject {
#Published var hasBeenPressed = false
}
struct WelcomeScreenButton: View {
#ObservedObject var welcomeButton = WelcomeButton()
var body: some View {
ZStack {
if welcomeButton.hasBeenPressed {
Circle()
.fill(Color(.white))
}
else {
Circle()
.fill(Color(.black))
}
}
}
}
struct ContentView: View {
#ObservedObject var welcomeButton = WelcomeButton()
var body: some View {
VStack {
Button(action: {welcomeButton.hasBeenPressed.toggle()})
{
WelcomeScreenButton()
}
.frame(width: 375, height: 375, alignment: .center)
}
}
}
You're very close, but you have to have the same instance of the WelcomeButton ObservableObject shared between the two. Right now, they're two separate instances, so when you update hasBeenPressed on one, the other one doesn't know to change its state.
struct WelcomeScreenButton: View {
#ObservedObject var welcomeButton : WelcomeButton //<-- here
var body: some View {
ZStack {
if welcomeButton.hasBeenPressed {
Circle()
.fill(Color(.white))
}
else {
Circle()
.fill(Color(.black))
}
}
}
}
struct ContentView: View {
#ObservedObject var welcomeButton = WelcomeButton()
var body: some View {
VStack {
Button(action: {welcomeButton.hasBeenPressed.toggle()})
{
WelcomeScreenButton(welcomeButton: welcomeButton) //<-- here
}
.frame(width: 375, height: 375, alignment: .center)
}
}
}
PS -- just as a general practice, it might be good to make the naming a little different just to keep things straight. WelcomeButton sounds like it's going to be a button. Naming it WelcomeButtonViewModel instead would make its intention more clear, although it wouldn't change the functionality at all.

Swift UI and ObservableObject Object Data Cleared

I'm a bit beginner in SWIFT and right now I'm facing a problem whit UI. Let me try to explain my problem.
my homeview screen data coming from web service using Observable object and it loads the data first time. But when I tried to open my left side slide menus than homeView webservice/obervable object data is just cleared when open the left slide menu view. Why my observable object data is empty. Let me share my code:
1.------ This is a my main/parentView
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.viewRouter.showSlideOutMenu = false
self.viewRouter.showDepartmentsMenu = false
self.viewRouter.showAccountMenu = false
}
}
}
return GeometryReader { g in
ZStack(alignment: .leading) {
RouteChanger(viewRouter: self._viewRouter)
if self.viewRouter.showSlideOutMenu {
MainMenuView(viewRouter: self._viewRouter)
.frame(width: g.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
}
}
2.----- This is my RouteChanger view for navigate to different pages of my views.
struct RouteChanger: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
GeometryReader { g in
VStack {
if self.viewRouter.currentPage == "Home" {
HomeView()
//.modifier(PageSwitchModifier())
}
}
}
}
}
3.... This is my homeView where I am using Observeable Object
struct HomeView: View {
#ObservedObject var homeController = HomeController()
var body: some View {
GeometryReader { g in
ZStack {
Color(UIColor.midTown.blue)
.edgesIgnoringSafeArea(.top)
VStack { //whole body
if self.homeController.homePageData.CODE == "0" {
ImageViewWidget(imageUrl: (self.homeController.homePageData.DATA?.headerList[0].img_url)!)
.frame(minWidth: g.size.width, maxWidth: g.size.width, minHeight: (g.size.width * UIImage(named: "header")!.size.height) / UIImage(named: "header")!.size.width, maxHeight: (g.size.width * UIImage(named: "header")!.size.height) / UIImage(named: "header")!.size.width)
}
else {
Text("Loading...")
.foregroundColor(Color.blue)
.padding()
.frame(width: g.size.width)
}
}
}
}
}
}
The EnvironmentObject is injected for all subviews automatically, so related part of your ContentView should look like below
ZStack(alignment: .leading) {
RouteChanger() // << here
if self.viewRouter.showSlideOutMenu {
MainMenuView() // << here
.frame(width: g.size.width/2)
.transition(.move(edge: .leading))
}

Resources