Rectangle overlay on hovering a custom button - swiftui

I'm trying to define a custom button style where on hover a rectangle pops up around that button.
struct CustomButtonStyle: ButtonStyle {
#State private var isOverButton = false
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color("Frost1"))
}
.padding(3)
.onHover { over in
self.isOverButton = over
print("isOverButton:", self.isOverButton, "over:", over)
}
.overlay(VStack {
if self.isOverButton {
Rectangle()
.stroke(Color("Frost1"), lineWidth: 2)
} else {
EmptyView()
}
})
}}
The print line shows me that setting the variable "isOverButton" is not working. Which type of variable state am I supposed to use that can be updated from "onHover" and updates "overlay"?

Here is a solution. Tested with Xcode 11.4.
struct TestOnHoverButton: View {
var body: some View {
Button("Button") {}
.buttonStyle(CustomButtonStyle())
}
}
struct CustomButtonStyle: ButtonStyle {
private struct CustomButtonStyleView<V: View>: View {
#State private var isOverButton = false
let content: () -> V
var body: some View {
ZStack {
content()
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.padding(3)
.onHover { over in
self.isOverButton = over
print("isOverButton:", self.isOverButton, "over:", over)
}
.overlay(VStack {
if self.isOverButton {
Rectangle()
.stroke(Color.blue, lineWidth: 2)
} else {
EmptyView()
}
})
}
}
func makeBody(configuration: Self.Configuration) -> some View {
CustomButtonStyleView { configuration.label }
}
}

Related

SwiftUI: Adapt size and position of one View to another View

I would like to adapt the position of one View (Rectangle) to another View (Button1). I only found solutions between a parent and a child-view or two child-views of the same parent view. Any tips how I could do that in this situation?
struct View: View {
var body: some View {
ZStack {
VStack {
Spacer()
HStack {
Spacer()
Button {} label: {
Text("1").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("2").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("3").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("4").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
}
Spacer()
}
ZStack {
Color.black.opacity(0.5)
.ignoresSafeArea()
Rectangle() // adapt this Rectangle to the button
.cornerRadius(10)
.frame(width: 200, height: 200)
.blendMode(.destinationOut)
}
.compositingGroup()
}
.background(Color.blue)
}
}
edit: I want to align the rectangle with the button to code a tutorial.
Thanks a lot!
This was the solution I found.
struct View: View {
#State private var sizeOfHole = CGSize()
#State private var positionOfHole = CGPoint()
var body: some View {
ZStack {
VStack {
Spacer()
HStack {
Spacer()
Button {
} label: {
Text("1").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
.readSize { size in
sizeOfHole = size
} // get the size of the button
.readPosition{ position in
positionOfHole = position
} // get the position of the button
Spacer()
Button {} label: {
Text("2").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("3").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
Button {} label: {
Text("4").foregroundColor(.white).padding().border(Color.white, width: 3).cornerRadius(5)
}
Spacer()
}
Spacer()
}
ZStack {
Color.black.opacity(0.5)
.ignoresSafeArea()
Rectangle()
.cornerRadius(10)
.frame(width: sizeOfHole.width + 10, height: sizeOfHole.height + 10) // set the size of the holw
.position(x: positionOfHole.x + (sizeOfHole.width / 2), y: positionOfHole.y - (sizeOfHole.height / 2)+1) // set the position of the hole
.blendMode(.destinationOut)
}
.compositingGroup()
}
.background(Color.blue)
}
}
// added this
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
extension View {
func readPosition(onChange: #escaping (CGPoint) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: PositionPreferenceKey.self, value: geometryProxy.frame(in: CoordinateSpace.global).origin)
}
)
.onPreferenceChange(PositionPreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
private struct PositionPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {}
}
Credit go to: Get width of a view using in SwiftUI
and https://stackoverflow.com/a/56520321/

Show BottomPlayerView above TabView in SwiftUI

I'm learning swiftUI and I want to make a music app.
I created a view which going to be above the tabView, but I want it to be shown only if user start playing a music.
My App, I use ZStack for bottomPlayer, and I share the bottomPlayer variable through .environmentObject(bottomPlayer) so the child views can use it:
class BottomPlayer: ObservableObject {
var show: Bool = false
}
#main
struct MyCurrentApp: App {
var bottomPlayer: BottomPlayer = BottomPlayer()
var audioPlayer = AudioPlayer()
var body: some Scene {
WindowGroup {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
TabBar()
if bottomPlayer.show {
BottomPlayerView()
.offset(y: -40)
}
}
.environmentObject(bottomPlayer)
}
}
}
The BottomPlayerView (above the TabView)
struct BottomPlayerView: View {
var body: some View {
HStack {
Image("cover")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text("Artist")
.foregroundColor(.orange)
Text("Song title")
.fontWeight(.bold)
}
Spacer()
Button {
print("button")
} label: {
Image(systemName: "play")
}
.frame(width: 60, height: 60)
}
.frame(maxWidth: .infinity, maxHeight: 60)
.background(Color.white)
.onTapGesture {
print("ontap")
}
}
}
My TabView:
struct TabBar: View {
var body: some View {
TabView {
AudiosTabBarView()
VideosTabBarView()
SearchTabBarView()
}
}
}
And In my SongsView, I use the EnvironmentObject to switch on the bottomPlayerView
struct SongsView: View {
#EnvironmentObject var bottomPlayer: BottomPlayer
var body: some View {
NavigationView {
VStack {
Button {
bottomPlayer.show = true
} label: {
Text("Show Player")
}
}
.listStyle(.plain)
.navigationBarTitle("Audios")
}
}
}
The problem is the bottomPlayer.show is actually set to true, but doesn't appear ...
Where I am wrong?
In your BottomPlayer add theĀ #Published attribute before the show boolean.
This creates a publisher of this type.
apple documentation

Overlay on top of inline navbar

Is it possible to overlay something on top of an inline nav bar? Here's an example with a popup where you can display and alert and then tap outside the alert to dismiss it.
I'd like the dark background overlay to also cover the nav bar. This works fine for the default large text style nav bar, but when I change it to an inline nav bar, the dark background no longer covers the nav. Is there a workaround for this?
import SwiftUI
struct ContentView: View {
#State private var isPresented = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
isPresented = true
}) {
Text("Show popup")
}
if isPresented {
ZStack {
Rectangle()
.foregroundColor(Color.black.opacity(0.5))
.edgesIgnoringSafeArea(.all)
.onTapGesture {
isPresented = false
}
Rectangle()
.foregroundColor(Color.red)
.frame(width: 300, height: 100)
.onTapGesture {
isPresented = true
}
Text("Alert!")
}
}
}
.navigationBarTitle("Hello", displayMode: .inline)
}
}
}
Wrapped NavigationView inside the ZStack.
struct ContentView: View {
#State private var isPresented = false
var body: some View {
ZStack { // < -- Here
NavigationView {
ZStack {
Button(action: {
isPresented = true
}) {
Text("Show popup")
}
}
.navigationBarTitle("Hello", displayMode: .inline)
}
if isPresented {
ZStack {
Rectangle()
.foregroundColor(Color.black.opacity(0.5))
.edgesIgnoringSafeArea(.all)
.onTapGesture {
isPresented = false
}
Rectangle()
.foregroundColor(Color.red)
.frame(width: 300, height: 100)
.onTapGesture {
isPresented = true
}
Text("Alert!")
}
}
}
}
}
Another way to use overlay.
struct ContentView: View {
#State private var isPresented = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
isPresented = true
}) {
Text("Show popup")
}
}
.navigationBarTitle("Hello", displayMode: .inline)
}.overlay( //<--- Here
alertView
)
}
#ViewBuilder
private var alertView: some View {
if isPresented {
ZStack {
Rectangle()
.foregroundColor(Color.black.opacity(0.5))
.edgesIgnoringSafeArea(.all)
.onTapGesture {
isPresented = false
}
Rectangle()
.foregroundColor(Color.red)
.frame(width: 300, height: 100)
.onTapGesture {
isPresented = true
}
Text("Alert!")
}
}
}
}

Using SwiftUI. My Slider/Side-menu launches new Views just fine when clicked but click <back> button and now all the options are 'dead'

Using SwiftUI and a slider/side menu tutorial that I have augmented in order to put actions on each of the side menu selections.
When the side menu is displayed and I tap a menu option, it works great and takes me to a new view with a menu item. But when i tap on and see the side menu still in place, all the menu items are not dead. The menu items still animate a click (with a flicker) but nothing happens. I have to close the side menu, reopen it, and then the menu items work once again - one time.
Can anyone tell me why this is happening?
Here is the pretty contentview, the mainview, and the sidemenu view.
//ContentView.swift
import SwiftUI
struct ContentView: View {
#State var showMenu = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}
return NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
MainView(showMenu: self.$showMenu)
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.showMenu ? geometry.size.width/2 : 0)
.disabled(self.showMenu ? true : false)
if self.showMenu {
MenuView()
.frame(width: geometry.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
.navigationBarTitle("Side Menu", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
withAnimation {
self.showMenu.toggle()
}
}) {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
))
}
}
}
struct MainView: View {
#Binding var showMenu: Bool
var body: some View {
Button(action: {
withAnimation {
self.showMenu = true
}
}) {
Text("Show Menu")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and here is the sidemenu view.
//MenuView.swift
import SwiftUI
struct PlayerView: View {
#State var showMenu = true
//#EnvironmentObject var session: SessionStore
var body: some View {
VStack{
//self.showMenu = true
Text("Manage Players Here").foregroundColor(.red)
}
}
}
struct MenuView: View {
#State var showMenu = true
var body: some View {
VStack(alignment: .leading) {
HStack() {
NavigationLink(destination: PlayerView()) {
HStack(){
Image(systemName: "person")
.foregroundColor(.gray)
.imageScale(.large)
Text("Players")
.foregroundColor(.gray)
.font(.headline)
}
}
}
.padding(.top, 100)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
.edgesIgnoringSafeArea(.all)
}
}
struct MenuView_Previews: PreviewProvider {
static var previews: some View {
MenuView()
}
}
enter code here
1) Binding var in the MenuView
2) OnAppear{} with Zstack to turn off the showMenu
struct ContentView: View {
#State var showMenu = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}
return NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
MainView(showMenu: self.$showMenu)
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.showMenu ? geometry.size.width/2 : 0)
.disabled(self.showMenu ? true : false)
if self.showMenu {
MenuView(showMenu: self.$showMenu)
.frame(width: geometry.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag).onAppear {
self.showMenu = false
}
}
.navigationBarTitle("Side Menu", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
withAnimation {
self.showMenu.toggle()
}
}) {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
))
}
}
}
struct MainView: View {
#Binding var showMenu: Bool
var body: some View {
Button(action: {
withAnimation {
self.showMenu = true
}
}) {
Text("Show Menu")
}
}
}
struct PlayerView: View {
#State var showMenu = true
//#EnvironmentObject var session: SessionStore
var body: some View {
VStack{
//self.showMenu = true
Text("Manage Players Here").foregroundColor(.red)
}
}
}
struct MenuView: View {
#Binding var showMenu: Bool // = true
var body: some View {
VStack(alignment: .leading) {
HStack() {
NavigationLink(destination: PlayerView()) {
HStack(){
Image(systemName: "person")
.foregroundColor(.gray)
.imageScale(.large)
Text("Players")
.foregroundColor(.gray)
.font(.headline)
}
}
}
.padding(.top, 100)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
.edgesIgnoringSafeArea(.all)
}
}

Button is not selectable on tvOS using own ButtonStyle in SwiftUI

After replacing a standard button style with a custom one, the button isn't selectable anymore on tvOS (it works as expected on iOS). Is there a special modifier in PlainButtonStyle() that I'm missing? Or is it a bug in SwiftUI?
Here's the snipped that works:
Button(
action: { },
label: { Text("Start") }
).buttonStyle(PlainButtonStyle())
and here's the one that doesn't:
Button(
action: { },
label: { Text("Start") }
).buttonStyle(RoundedButtonStyle())
where RoundedButtonStyle() is defined as:
struct RoundedButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(6)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(100)
}
}
If you create your own style you have to handle focus manual. Of course there are different ways how you could do this.
struct RoundedButtonStyle: ButtonStyle {
let focused: Bool
func makeBody(configuration: Configuration) -> some View {
configuration
.label
.padding(6)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(100)
.shadow(color: .black, radius: self.focused ? 20 : 0, x: 0, y: 0) // 0
}
}
struct ContentView: View {
#State private var buttonFocus: Bool = false
var body: some View {
VStack {
Text("Hello World")
Button(
action: { },
label: { Text("Start") }
).buttonStyle(RoundedButtonStyle(focused: buttonFocus))
.focusable(true) { (value) in
self.buttonFocus = value
}
}
}
}
I have done it in this way and it's working fine, what you need to do is just handling the focused state.
struct AppButtonStyle: ButtonStyle {
let color: Color = .clear
func makeBody(configuration: Configuration) -> some View {
return AppButton(configuration: configuration, color: color)
}
struct AppButton: View {
#State var focused: Bool = false
let configuration: ButtonStyle.Configuration
let color: Color
var body: some View {
configuration.label
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 20).fill(color))
.compositingGroup()
.shadow(color: .black, radius: 5)
.scaleEffect(focused ? 1.1 : 1.0)
.padding()
.focusable(true) { focused in
withAnimation {
self.focused = focused
}
}
}
}
}
When the button is getting focused I'm just scaling it and you can do something else as you wish with the same idea, so let's say you wan't to change the background color:
.background(RoundedRectangle(cornerRadius: 20).fill(focused ? .red : .white))
To use:
struct SomeView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: Text("Destination")) {
Text("Hi")
}
.buttonStyle(AppButtonStyle())
.padding()
}
}
}