AnyTransition issue with simple view update in macOS - swift

I have 2 user view called user1 and user2, I am updating user with button, and I want give a transition animation to update, but for some reason my transition does not work, as I wanted, the issue is there that Text animated correctly but image does not, it stay in its place and it does not move with Text to give a smooth transition animation.
struct ContentView: View {
#State var show: Bool = Bool()
var body: some View {
VStack {
if (show) {
UserView(label: { Text("User 1") })
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.trailing), removal: AnyTransition.move(edge: Edge.leading)))
}
else {
UserView(label: { Text("User 2") })
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.leading), removal: AnyTransition.move(edge: Edge.trailing)))
}
Button("update") { show.toggle() }
}
.padding()
.animation(Animation.linear(duration: 1.0), value: show)
}
}
struct UserView<Label: View>: View {
let label: () -> Label
#State private var heightOfLabel: CGFloat? = nil
var body: some View {
HStack {
if let unwrappedHeight: CGFloat = heightOfLabel {
Image(systemName: "person")
.resizable()
.frame(width: unwrappedHeight, height: unwrappedHeight)
}
label()
.background(GeometryReader { proxy in
Color.clear
.onAppear(perform: { heightOfLabel = proxy.size.height })
})
Spacer(minLength: CGFloat.zero)
}
.animation(nil, value: heightOfLabel)
}
}

the heightOfLabel doesn't have to be optional, and then it works:
struct UserView<Label: View>: View {
let label: () -> Label
#State private var heightOfLabel: CGFloat = .zero // not optional
var body: some View {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: heightOfLabel, height: heightOfLabel)
label()
.background(GeometryReader { proxy in
Color.clear
.onAppear(perform: { heightOfLabel = proxy.size.height })
})
Spacer(minLength: CGFloat.zero)
}
.animation(nil, value: heightOfLabel)
}
}

Related

How to hide a swiftUI view by touching anywhere outside of it

I have a view that call an alert that is another smaller view, whenever the second View is shown, I want to hide it when clicking outside of it.
How can I do that?
struct AlertView: View{
let screenSize = UIScreen.main.bounds
#Binding var alertIsShown: Bool
var body: some View{
VStack{
Button("Cancel") {
self.alertIsShown=false
}
}.padding()
.frame(width: screenSize.width * 0.85, height: screenSize.height * 0.6)
.background(Color(red: 0.4627, green: 0.8392, blue: 1.0))
.clipShape(RoundedRectangle(cornerRadius: 20.0, style: .continuous))
.offset(y: alertIsShown ? 0 : screenSize.height)
.animation(.spring())
.shadow(color: Color(.white), radius: 6, x: -9, y: -0)
}
}
}
For the main view that call the alert:
struct MainView: View {
#State private var alertIsShown = false
#State var liveOrdersList: [String] = ["item-1", "item-2"]
var body: some View {
VStack{
NavigationView{
List{
ForEach(liveOrdersList, id: \.self) { order in
HStack {
VStack(alignment: .leading){
Text("\(order.totalPrice)")
}
Spacer()
Button("add") {
withAnimation(.linear(duration: 0.3)) {
alertIsShown.toggle()
}
}
}
}
}
}
}
}
if alertIsShown{ //here I call the aler
AlertView(alertIsShown: $alertIsShown)
}
}
The list of buttons call the alert view.
How can I hide it when tapping outside of it?
You could try this approach, using .simultaneousGesture(...), as shown in this example code, to hide the AlertView by touching anywhere outside of it.
struct ContentView: View {
var body: some View {
MainView()
}
}
struct MainView: View {
#State var alertIsShown = false
// for testing
#State var liveOrdersList: [String] = ["item-1", "item-2", "item-3", "item-4", "item-5"]
var body: some View {
NavigationView {
VStack {
List {
ForEach(liveOrdersList, id: \.self) { order in
HStack {
VStack(alignment: .leading) {
Text("\(order)")
}
Spacer()
Button("add") {
withAnimation(.linear(duration: 0.3)) {
alertIsShown.toggle()
}
}
}
}
}
// -- here
.simultaneousGesture(alertIsShown ? TapGesture().onEnded {
alertIsShown = false
} : nil)
if alertIsShown {
AlertView(alertIsShown: $alertIsShown)
}
}
}
}
}

How to remove the cornerradius of sheets in swiftui?

Is there a way to remove the cornerRadius of a sheet? I tried it like this:
.sheet(isPresented: $showModal) {
Modal().cornerRadius(0, corners: [.topLeft, .topRight])
}
but it didn't work.
I know I can just use fullScreenCover but I still want to know if there is a solution to this.
According to my comment above you can create your own slide-in menu.
In the example below I added a close button as well as gesture control to close the view.
//
//
// SlideInMenu.swift
// SlideInMenu
//
// Created by Sebastian on 21.09.22.
//
import SwiftUI
var bounds = UIScreen.main.bounds
struct ContentView: View {
#State var selectedItem: String = ""
#State var showMenu = false
var body: some View {
ZStack() {
MainView(selectedItem: $selectedItem, showMenu: $showMenu)
.blur(radius: showMenu ? 3 : 0)
SlideView(selectedItem: $selectedItem, showMenu: $showMenu)
}
.edgesIgnoringSafeArea(.all)
}
}
struct MainView: View {
#Binding var selectedItem: String
#Binding var showMenu: Bool
var body: some View {
HStack(){
Spacer()
VStack() {
Spacer()
Text("This is your main View")
.foregroundColor(.white)
.padding()
Button(action: {
withAnimation(.linear(duration: 0.3)) {
self.showMenu.toggle()
}
}) {
Text("Show Menu")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
}
Spacer()
}
Spacer()
}.background(Color.blue)
}
}
struct SlideView: View {
#Binding var selectedItem: String
#Binding var showMenu: Bool
#State private var viewOffest: CGFloat = 100
#State private var offset = CGSize.zero
#State private var isDragging = false
var body: some View {
let dragGesture = DragGesture()
.onChanged { value in
withAnimation(.linear(duration: 0.2)) {
if value.translation.height >= 0 {
offset = value.translation
}
}
}
.onEnded { _ in
withAnimation(.linear(duration: 0.2)) {
isDragging = false
if offset.height > (bounds.height - viewOffest)/3 {
showMenu.toggle()
}
offset = .zero
}
}
ZStack() {
Color.black
.opacity(showMenu ? 0.5 : 0)
VStack() {
HStack() {
Spacer()
Spacer()
}
VStack(alignment: .leading) {
HStack() {
Spacer()
Text("Here is the menu")
.foregroundColor(.black)
Spacer()
}
HStack() {
Spacer()
Button(action: {
withAnimation(.linear(duration: 0.3)) {
self.showMenu.toggle()
}
}) {
Text("Close Menu")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.red)
}
.padding()
Spacer()
}
Spacer()
}
.padding()
.background(Color.white)
.cornerRadius(0)
}
.offset(y: showMenu ? viewOffest + offset.height : bounds.height)
.gesture(dragGesture)
}
}
}
It is not possible for now perhaps we can in further update, nonetheless you can create your own custom view.

Best way to trigger a button events using #State and #Binding

Essentially I would like to trigger the MainButton in ContentView to Open SheetView and then use NavCloseButton to Close the SheetView to Return Back to ContentView. I've been trying to do this using #State and #Binding. While getting SheetView presented using .sheet(isPresented: is simple I'm having trouble dismissing it when the buttons are extracted out.
Can someone please show example how these actions would be performed?
ContentView:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Spacer()
Image(systemName: "hand.thumbsup.circle.fill")
.resizable()
.frame(width: 200, height: 200)
.symbolRenderingMode(.hierarchical)
Spacer()
MainButton(color: .blue, title: "Tap to Open", image: "lock.open.laptopcomputer")
}
.navigationTitle("Page One")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}
SheetView:
struct SheetView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
}
.navigationTitle("Sheet View")
.navigationBarItems(trailing: NavCloseButton(color: .red,
title: "Close",
image: ""))
}
}
}
struct SheetView_Previews: PreviewProvider {
static var previews: some View {
SheetView()
.preferredColorScheme(.dark)
}
}
AppButtons:
struct NavCloseButton: View {
var color: Color
var title: String
var image: String
var body: some View {
Button {
print("Closing")
} label: {
Text(title)
.padding()
.frame(width: 100, height: 40)
.background(color)
.foregroundColor(.white)
.cornerRadius(10)
.font(.system(.body))
}
}
}
struct MainButton: View {
var color: Color
var title: String
var image: String
var body: some View {
Button {
print("Opened")
} label: {
Label {
Text(title)
} icon: {
Image(systemName: image)
}
.padding()
.frame(width: 350, height: 60)
.background(color)
.foregroundColor(.white)
.cornerRadius(15)
.font(.title2)
}
}
}
Following your code this is how you would do it.
Changes are commented.
struct ContentView: View {
#State private var showSheet = false // define state
var body: some View {
NavigationView {
VStack {
Spacer()
Image(systemName: "hand.thumbsup.circle.fill")
.resizable()
.frame(width: 200, height: 200)
.symbolRenderingMode(.hierarchical)
Spacer()
MainButton(color: .blue, title: "Tap to Open", image: "lock.open.laptopcomputer", showSheet: $showSheet) // pass binding
}
.navigationTitle("Page One")
.sheet(isPresented: $showSheet) { // present sheet
SheetView(showSheet: $showSheet) // pass binding
}
}
}
}
struct SheetView: View {
#Binding var showSheet: Bool // pass binding here
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
}
.navigationTitle("Sheet View")
.navigationBarItems(
trailing:
NavCloseButton(color: .red,
title: "Close",
image: "",
showSheet: $showSheet)) // pass binding
}
}
}
struct NavCloseButton: View {
var color: Color
var title: String
var image: String
#Binding var showSheet: Bool // pass binding here
var body: some View {
Button {
print("Closing")
showSheet = false // set binding to dismiss sheet
} label: {
Text(title)
.padding()
.frame(width: 100, height: 40)
.background(color)
.foregroundColor(.white)
.cornerRadius(10)
.font(.system(.body))
}
}
}
struct MainButton: View {
var color: Color
var title: String
var image: String
#Binding var showSheet: Bool // pass binding here
var body: some View {
Button {
print("Opened")
showSheet = true // set binding to show sheet
} label: {
Label {
Text(title)
} icon: {
Image(systemName: image)
}
.padding()
.frame(width: 350, height: 60)
.background(color)
.foregroundColor(.white)
.cornerRadius(15)
.font(.title2)
}
}
}
But it might be easier to change the buttons to vars or define custom Button styles.
Here is another variant with one CutomButton that is taking a button action as a closure, reducing the Binding passing around:
struct ContentView: View {
#State private var showSheet = false // define state
var body: some View {
NavigationView {
VStack {
Spacer()
Image(systemName: "hand.thumbsup.circle.fill")
.resizable()
.frame(width: 200, height: 200)
.symbolRenderingMode(.hierarchical)
Spacer()
CustomButton(color: .blue, title: "Tap to Open", image: "lock.open.laptopcomputer") {
showSheet = true // passed button action
}
.font(.title2)
}
.navigationTitle("Page One")
.sheet(isPresented: $showSheet) { // present sheet
SheetView(showSheet: $showSheet) // pass binding
}
}
}
}
struct SheetView: View {
#Binding var showSheet: Bool // pass binding here
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
}
.navigationTitle("Sheet View")
.navigationBarItems(
trailing:
CustomButton(color: .red, title: "Close") {
showSheet = false // passed button action
}
)
}
}
}
struct CustomButton: View {
let color: Color
let title: String
var image: String? = nil
let action: () -> Void // button action (as trailing closure)
var body: some View {
Button {
action() // run the passed action
} label: {
Group {
if let image {
Label(title, systemImage: image)
} else {
Text(title)
}
}
.padding()
.background(color)
.foregroundColor(.white)
.cornerRadius(15)
}
}
}

Fit GeometryReader to drawer contents

How can I fit the GeometryReader to the middle ExpandingDrawer contents?
(Copy & Paste-able):
import SwiftUI
struct ExpandingDrawerButton: View {
#Binding var isExpanded: Bool
var body: some View {
Button(action: { withAnimation { isExpanded.toggle() } }) {
Text(isExpanded ? "Close" : "Open")
}
}
}
struct ExpandingDrawer<Content: View>: View {
#Binding var isExpanded: Bool
var content: () -> Content
var body: some View {
content()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: nil, maxHeight: contentHeight)
.allowsHitTesting(isExpanded)
.clipped()
.transition(.slide)
}
private var contentHeight: CGFloat? {
isExpanded ? nil : CGFloat(0)
}
}
struct DrawerTestView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
struct ContentView: View {
#State var isExpanded = false
var body: some View {
GeometryReader { geo in
VStack(spacing: 0) {
top
.frame(height: geo.size.height * 1/4)
middle
bottom
.frame(height: geo.size.width)
}
}
}
var top: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
Rectangle()
.foregroundColor(.blue.opacity(0.2))
ExpandingDrawerButton(isExpanded: $isExpanded)
.padding()
}
}
var middle: some View {
ExpandingDrawer(isExpanded: $isExpanded) {
middleContent
}
}
var middleContent: some View {
GeometryReader { geo in
VStack {
ForEach(0..<10) { _ in
Button(action: {}) { Text("Random shit") }
}
Text("Don't know how tall...")
Text("Height can change...")
Text("But does need to fit snug (no extra space)")
}
}
}
var bottom: some View {
ZStack {
Rectangle()
.foregroundColor(.red)
.aspectRatio(1, contentMode: .fit)
VStack {
Text("Needs to be a square...")
Text("Okay if pushed below edge of screen...")
}
}
}
}
}
I've tried various combinations of .fixedSize() and .aspectRatio() but I'm struggling...
To avoid GeometryReader changing our views, we can put it in an .overlay().
Example:
struct ContentView: View {
class Storage {
var geo: GeometryProxy! {
didSet {
print(geo.size)
}
}
}
let storage = Storage()
/* ... */
}
var middleContent: some View {
VStack {
ForEach(0..<10) { _ in
Button(action: {}) { Text("Random stuff") }
}
Text("Don't know how tall...")
Text("Height can change...")
Text("But does need to fit snug (no extra space)")
}
.overlay(
GeometryReader { geo in
Rectangle()
.fill(Color.clear)
let _ = storage.geo = geo
}
)
}
You can now use storage.geo to access the GeometryProxy of this view.

Tabbar middle button utility function in SwiftUI

I'm trying to reproduce a "Instagram" like tabBar which has a "Utility" button in the middle which doesn't necessarily belong to the tabBar eco system.
I have attached this gif to show the behaviour I am after. To describe the issue. The tab bar in the middle (Black plus) is click a ActionSheet is presented INSTEAD of switching the view.
How I would do this in UIKit is simply use the
override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
print("Selected item")
}
Function from the UITabBarDelegate. But obviously we can't do this in SwiftUI so was looking to see if there was any ideas people have tried. My last thought would be to simply wrap it in a UIView and use it with SwiftUI but would like to avoid this and keep it native.
I have seen a write up in a custom TabBar but would like to use the TabBar provided by Apple to avoid any future discrepancies.
Thanks!
Edit: Make the question clearer.
Thanks to Aleskey for the great answer (Marked as correct). I evolved it a little bit in addition to a medium article that was written around a Modal. I found it to be a little different
Here's the jist.
A MainTabBarData which is an Observable Object
final class MainTabBarData: ObservableObject {
/// This is the index of the item that fires a custom action
let customActiontemindex: Int
let objectWillChange = PassthroughSubject<MainTabBarData, Never>()
var previousItem: Int
var itemSelected: Int {
didSet {
if itemSelected == customActiontemindex {
previousItem = oldValue
itemSelected = oldValue
isCustomItemSelected = true
}
objectWillChange.send(self)
}
}
func reset() {
itemSelected = previousItem
objectWillChange.send(self)
}
/// This is true when the user has selected the Item with the custom action
var isCustomItemSelected: Bool = false
init(initialIndex: Int = 1, customItemIndex: Int) {
self.customActiontemindex = customItemIndex
self.itemSelected = initialIndex
self.previousItem = initialIndex
}
}
And this is the TabbedView
struct TabbedView: View {
#ObservedObject private var tabData = MainTabBarData(initialIndex: 1, customItemIndex: 2)
var body: some View {
TabView(selection: $tabData.itemSelected) {
Text("First Screen")
.tabItem {
VStack {
Image(systemName: "globe")
.font(.system(size: 22))
Text("Profile")
}
}.tag(1)
Text("Second Screen")
.tabItem {
VStack {
Image(systemName: "plus.circle")
.font(.system(size: 22))
Text("Profile")
}
}.tag(2)
Text("Third Screen")
.tabItem {
VStack {
Image(systemName: "number")
.font(.system(size: 22))
Text("Profile")
}
}.tag(3)
}.actionSheet(isPresented: $tabData.isCustomItemSelected) {
ActionSheet(title: Text("SwiftUI ActionSheet"), message: Text("Action Sheet Example"),
buttons: [
.default(Text("Option 1"), action: option1),
.default(Text("Option 2"), action: option2),
.cancel(cancel)
]
)
}
}
func option1() {
tabData.reset()
// ...
}
func option2() {
tabData.reset()
// ...
}
func cancel() {
tabData.reset()
}
}
struct TabbedView_Previews: PreviewProvider {
static var previews: some View {
TabbedView()
}
}
Similar concept, just uses the power of SwiftUI and Combine.
You could introduce new #State property for storing old tag of presented tab. And perform the next method for each of your tabs .onAppear { self.oldSelectedItem = self.selectedItem } except the middle tab. The middle tab will be responsible for showing the action sheet and its method will look the following:
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Working example:
import SwiftUI
struct ContentView: View {
#State private var selectedItem = 1
#State private var shouldShowActionSheet = false
#State private var oldSelectedItem = 1
var body: some View {
TabView (selection: $selectedItem) {
Text("Home")
.tabItem { Image(systemName: "house") }
.tag(1)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Search")
.tabItem { Image(systemName: "magnifyingglass") }
.tag(2)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Add")
.tabItem { Image(systemName: "plus.circle") }
.tag(3)
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Text("Heart")
.tabItem { Image(systemName: "heart") }
.tag(4)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Profile")
.tabItem { Image(systemName: "person.crop.circle") }
.tag(5)
.onAppear { self.oldSelectedItem = self.selectedItem }
}
.actionSheet(isPresented: $shouldShowActionSheet) { ActionSheet(title: Text("Title"), message: Text("Message"), buttons: [.default(Text("Option 1"), action: option1), .default(Text("Option 2"), action: option2) , .cancel()]) }
}
func option1() {
// do logic 1
}
func option2() {
// do logic 2
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Previous answers did not help me so I'm pasting my complete solution.
import SwiftUI
import UIKit
enum Tab {
case map
case recorded
}
#main
struct MyApp: App {
#State private var selectedTab: Tab = .map
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
VStack {
switch selectedTab {
case .map:
NavigationView {
FirstView()
}
case .recorded:
NavigationView {
SecondView()
}
}
CustomTabView(selectedTab: $selectedTab)
.frame(height: 50)
}
}
}
}
struct FirstView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("First view")
}
}
struct SecondView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("second view")
}
}
struct CustomTabView: View {
#Binding var selectedTab: Tab
var body: some View {
HStack {
Spacer()
Button {
selectedTab = .map
} label: {
VStack {
Image(systemName: "map")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Map")
.font(.caption2)
}
.foregroundColor(selectedTab == .map ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
Button {
} label: {
ZStack {
Circle()
.foregroundColor(.secondary)
.frame(width: 80, height: 80)
.shadow(radius: 2)
Image(systemName: "plus.circle.fill")
.resizable()
.foregroundColor(.primary)
.frame(width: 72, height: 72)
}
.offset(y: -2)
}
Spacer()
Button {
selectedTab = .recorded
} label: {
VStack {
Image(systemName: "chart.bar")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Recorded")
.font(.caption2)
}
.foregroundColor(selectedTab == .recorded ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
}
}
}