Assign the value of ButtonStyle configuration.isPressed to other variable - swift

I have 2 questions, first, I have this code and I'm trying to assign the value of configuration.isPressed (in ButtonStyle ) to the variable #State var isPressed
import SwiftUI
import ARKit
struct ContentView: View {
#State var isPressed: Bool = false
var body: some View {
ZStack {
VStack {
Spacer()
Button(action: {}) {
Text("Hello World")
}
.buttonStyle(MyButtonStyle(p: $isPressed))
}
}
}
}
struct MyButtonStyle: ButtonStyle {
#Binding var p: Bool
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(8.0)
if configuration.isPressed {
////CODE HERE ?
}
}
}
Is the above possible?
Second questions: I found plenty of ways to detect when a button is pressed and released. Is the usage ButtonStyle a good practice/way to achieve this goal?
Thanks in advance!

Related

Custom modifier disables the Controls (Picker and Stepper) | SwiftUI

When, I apply custom modififer .settingBlockModifier(), it disables controllers in the View. I've also tried this way .modifier(SettingBlockModifier()).
Checked, the same behavior also, if I apply the same modifiers without custom modifier. Here's the effect I want to achieve.
The problem that the Picker and Stepper don't work, they are not tappable. How to solve it?
struct TestFile: View {
#State private var gamesAmount = ["5", "10", "20", "30"]
#State private var game: Int = 1
#State private var number = 1
var body: some View {
VStack {
VStack {
Text("How many questions?")
.bold()
Picker("How many questions?",selection: $game) {
ForEach(0..<gamesAmount.count) {
Text(gamesAmount[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}
.settingBlockModifier()
HStack {
Text("Number:")
.padding(.trailing, 40)
Stepper("\(number)", value: $number, in: 1...10)
}
.settingBlockModifier()
//.modifier(SettingBlockModifier())
}
}
}
struct SettingBlockModfier: ViewModifier {
func body(content: Content) -> some View {
content
.background(Color.white.cornerRadius(8))
.shadow(radius: 8)
.padding()
}
}
extension View {
func settingBlockModifier() -> some View {
self.modifier(SettingBlockModfier())
}
}
You can use compositingGroup:
struct SettingBlockModfier: ViewModifier {
func body(content: Content) -> some View {
content
.compositingGroup() // <- here
.background(Color.white.cornerRadius(8))
.shadow(radius: 8)
.padding()
}
}

SwiftUI: Optimal way to disable NavigationLink which has a custom ButtonStyle

I have a navigation link which has a custom ButtonStyle:
NavigationLink(destination: NextScreen()) {
Text("Next")
}
.buttonStyle(CustomButtonStyle(disabled: !isValidPassword))
And my CustomButtonStyle looks like this:
#State var disabled = false
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding(15)
.foregroundColor(.white)
.background(disabled ? .black : .gray)
.cornerRadius(40)
.disabled(disabled) // this has no effect when inside a NavigationLink
}
The UI updates correctly as the user types in the password.
You can see that I disable the button inside the ButtonStyle, but this doesn't prevent the user from still tapping the NavigationLink to go to NextScreen().
To fix this I end up doing this:
NavigationLink(destination: SignupStepBirthdayView()) {
Text("Next")
}
.buttonStyle(BobbleUpButtonStyle(disabled: !isValidPassword))
.disabled(!isValidPassword)
Which seems inefficient as I'm passing a disabled state to the button style to update the UI, and then having to disable the actual NavigationLink.
Is there a better way to do this?
I will show you simple way, no needed to using #Bingding or #State
First, create your button style:
struct CustomButtonStyle: ButtonStyle {
public func makeBody(configuration: ButtonStyle.Configuration) -> some View {
MyButton(configuration: configuration)
}
struct MyButton: View {
let configuration: ButtonStyle.Configuration
#Environment(\.isEnabled) private var isEnabled: Bool
var body: some View {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding(15)
.foregroundColor(.white)
.background(isEnabled ? Color.blue : Color.gray)
.cornerRadius(40)
.disabled(!isEnabled)
}
}
}
Then, disable it as any SwiftUI component:
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter your text", text: $text)
NavigationLink(destination: NextScreen()) {
Text("Next")
}
.buttonStyle(CustomButtonStyle())
.disabled(text.isEmpty)
}
}
}
}
You have to use Binding instead of State in CustomButtonStyle
#Binding var disabled : Bool
And this changes in your ContentView
#State var isValidPassword: Bool = true
also:
.buttonStyle(BobbleUpButtonStyle(disabled: $isValidPassword))

SWIFTUI - How to keep score in a quiz app with questions on separate views?

Newbie here. I am creating a quiz app. Unlike most of the examples, every question is in a separate view. I'd like to keep the score and show it at the end.
Example:
struct questionOne: View {
#State var isSelected = false
var body: some View {
GeometryReader { geometryProxy in
VStack(alignment: .center) {
TRpic().cornerRadius(10)
Text("What's the capital of Turkey?")
.font(.title)
Spacer()
Button(action: {self.isSelected.toggle()}) {
Text("Istanbul")
} .buttonStyle(SelectedButtonStyleFalse(isSelected: self.$isSelected))
Button(action: {self.isSelected.toggle()}) {
Text("Ankara")
}.buttonStyle(SelectedButtonStyle(isSelected: self.$isSelected))
Button(action: {self.isSelected.toggle()}) {
Text("Athens")
} .buttonStyle(SelectedButtonStyleFalse(isSelected: self.$isSelected))
Spacer()
NavigationLink(destination: questionTwo()) {
VStack {
Text("Next Question")
Adview().frame(width: 150, height: 50)
}
}
}
}
}
}
struct SelectedButtonStyle: ButtonStyle {
#Binding var isSelected: Bool
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(20)
.foregroundColor(.white)
.background(isSelected ? Color.green : Color.gray)
.cornerRadius(10.0)
}
}
struct SelectedButtonStyleFalse: ButtonStyle {
#Binding var isSelected: Bool
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(20)
.foregroundColor(.white)
.background(isSelected ? Color.red : Color.gray)
.cornerRadius(10.0)
}
}
I have two button styles: SelectedButtonStyle (for the true answer) and SelectedButtonStyleFalse (for the false answer)
All 50 questions (views) are connected via NavigationView. And in the end, I want to show the total score in a separate view. For example "You have scored 35/50".
Thanks!
You can use #EnvironmentObject.
All you have to do it create a class like this:
import Foundation
class GameStatus: ObservableObject {
#Published var score: Int = 0
}
Then in your struct add #EnvironmentObject var gameStatus: GameStatus
Finally, you need to trigger update on the score, just do
self.gameStatus.answers += 1
Check out Three Ways To Do The Same Job for 3 examples of how to share data between views.

SwiftUI: Generic parameter 'Subject' could not be inferred

I built a LoadingView with SwiftUI for showing some loading stuff in my app while I'm fetching remote data from an API. I am on Xcode Version 11.0 beta 5.
This is the LoadingView:
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text("Loading...")
ActivityIndicator(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.white)
.foregroundColor(Color.primary)
.cornerRadius(5)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
This is my DataStore. It is declared as ObservableObject and has more than one #Published property. Also it does some remote fetching from an API:
class CharacterStore: ObservableObject {
#Published private(set) var isLoading = false
// Fetches some stuff from a remote api
func fetch() {
self.isLoading = true
myService.getCharacters { (result) in
DispatchQueue.main.async {
self.isLoading = false
}
}
}
}
And finally this is the View I want to show my LoadingView with the content of ContentView in it. Of course I am setting the #EnvironmentObject before showing this view.
struct ContentView: View {
#EnvironmentObject var charStore: CharacterStore
var body: some View {
LoadingView(isShowing: self.$charStore.isLoading) { // Here I get the error
// Show some Content here
Text("")
}
}
}
The problem is that I want to bind self.$charStore.isLoading to LoadingView. In this line i get the following error:
Generic parameter 'Subject' could not be inferred
I tried in several ways but none of these things work. Btw: If I use a #State property in ContentView it just works fine like this:
struct ContentView: View {
#EnvironmentObject var charStore: CharacterStore
#State var loads: Bool = false
var body: some View {
LoadingView(isShowing: self.$loads) { // Here I get no error
// Show some Content here
Text("")
}
}
}
Am I missing a thing? If you need further informations let me know i can provide more content if needed.
Thanks for the help!
Since your LoadingView is not going to modify .isLoading, you do not need to pass it as a binding:
LoadingView(isShowing: self.$charStore.isLoading)
Instead, remove the #Binding in LoadingView:
struct LoadingView<Content>: View where Content: View {
var isShowing: Bool
...
and create it like this (remove the dollar sign):
LoadingView(isShowing: self.charStore.isLoading) { ... }
On the contrary, if you insist on passing a binding, then you need to remove the private(set) from:
#Published private(set) var isLoading = false
Couldn't you do the following:
Replacing the #Binding by the same #EnvironmentObject as the ContentView uses.
struct LoadingView<Content>: View where Content: View {
#EnvirontmentObject var charStore: CharacterStore // added
//#Binding var isShowing: Bool // removed
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.$charStore.isLoading) // Changed
.blur(radius: self.$charStore.isLoading ? 3 : 0) // Changed
VStack {
Text("Loading...")
ActivityIndicator(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.white)
.foregroundColor(Color.primary)
.cornerRadius(5)
.opacity(self.$charStore.isLoading ? 1 : 0) // Changed
}
}
}
}
Of course, you also have to remove the isShowing parameter from the LoadingView() initializer in the ContentView.
Please correct me if I am wrong!

SwiftUI: Global Overlay That Can Be Triggered From Any View

I'm quite new to the SwiftUI framework and I haven't wrapped my head around all of it yet so please bear with me.
Is there a way to trigger an "overlay view" from inside "another view" when its binding changes? See illustration below:
I figure this "overlay view" would wrap all my views. I'm not sure how to do this yet - maybe using ZIndex. I also guess I'd need some sort of callback when the binding changes, but I'm also not sure how to do that either.
This is what I've got so far:
ContentView
struct ContentView : View {
#State private var liked: Bool = false
var body: some View {
VStack {
LikeButton(liked: $liked)
}
}
}
LikeButton
struct LikeButton : View {
#Binding var liked: Bool
var body: some View {
Button(action: { self.toggleLiked() }) {
Image(systemName: liked ? "heart" : "heart.fill")
}
}
private func toggleLiked() {
self.liked = !self.liked
// NEED SOME SORT OF TOAST CALLBACK HERE
}
}
I feel like I need some sort of callback inside my LikeButton, but I'm not sure how this all works in Swift.
Any help with this would be appreciated. Thanks in advance!
It's quite easy - and entertaining - to build a "toast" in SwiftUI!
Let's do it!
struct Toast<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 toast
let presenting: () -> Presenting
/// The text to show
let text: Text
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.presenting()
.blur(radius: self.isShowing ? 1 : 0)
VStack {
self.text
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
Explanation of the body:
GeometryReader gives us the preferred size of the superview , thus allowing the perfect sizing for our Toast.
ZStack stacks views on top of each other.
The logic is trivial: if the toast is not supposed to be seen (isShowing == false), then we render the presenting view. If the toast has to be presented (isShowing == true), then we render the presenting view with a little bit of blur - because we can - and we create our toast next.
The toast is just a VStack with a Text, with custom frame sizing, some design bells and whistles (colors and corner radius), and a default slide transition.
I added this method on View to make the Toast creation easier:
extension View {
func toast(isShowing: Binding<Bool>, text: Text) -> some View {
Toast(isShowing: isShowing,
presenting: { self },
text: text)
}
}
And a little demo on how to use it:
struct ContentView: View {
#State var showToast: Bool = false
var body: some View {
NavigationView {
List(0..<100) { item in
Text("\(item)")
}
.navigationBarTitle(Text("A List"), displayMode: .large)
.navigationBarItems(trailing: Button(action: {
withAnimation {
self.showToast.toggle()
}
}){
Text("Toggle toast")
})
}
.toast(isShowing: $showToast, text: Text("Hello toast!"))
}
}
I used a NavigationView to make sure the view fills the entire screen, so the Toast is sized and positioned correctly.
The withAnimation block ensures the Toast transition is applied.
How it looks:
It's easy to extend the Toast with the power of SwiftUI DSL.
The Text property can easily become a #ViewBuilder closure to accomodate the most extravagant of the layouts.
To add it to your content view:
struct ContentView : View {
#State private var liked: Bool = false
var body: some View {
VStack {
LikeButton(liked: $liked)
}
// make it bigger by using "frame" or wrapping it in "NavigationView"
.toast(isShowing: $liked, text: Text("Hello toast!"))
}
}
How to hide the toast afte 2 seconds (as requested):
Append this code after .transition(.slide) in the toast VStack.
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
self.isShowing = false
}
}
}
Tested on Xcode 11.1
I modified Matteo Pacini's great answer, above, incorporating comments to have the Toast fade in and fade out after a delay. I also modified the View extension to be a bit more generic, and to accept a trailing closure similar to the way .sheet works.
ContentView.swift:
struct ContentView: View {
#State private var lightsOn: Bool = false
#State private var showToast: Bool = false
var body: some View {
VStack {
Button(action: {
if (!self.showToast) {
self.lightsOn.toggle()
withAnimation {
self.showToast = true
}
}
}){
Text("switch")
} //Button
.padding(.top)
Image(systemName: self.lightsOn ? "lightbulb" : "lightbulb.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.all)
.toast(isPresented: self.$showToast) {
HStack {
Text("Lights: \(self.lightsOn ? "ON" : "OFF")")
Image(systemName: self.lightsOn ? "lightbulb" : "lightbulb.fill")
} //HStack
} //toast
} //VStack
} //body
} //ContentView
View+Toast.swift:
extension View {
func toast<Content>(isPresented: Binding<Bool>, content: #escaping () -> Content) -> some View where Content: View {
Toast(
isPresented: isPresented,
presenter: { self },
content: content
)
}
}
Toast.swift:
struct Toast<Presenting, Content>: View where Presenting: View, Content: View {
#Binding var isPresented: Bool
let presenter: () -> Presenting
let content: () -> Content
let delay: TimeInterval = 2
var body: some View {
if self.isPresented {
DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
withAnimation {
self.isPresented = false
}
}
}
return GeometryReader { geometry in
ZStack(alignment: .bottom) {
self.presenter()
ZStack {
Capsule()
.fill(Color.gray)
self.content()
} //ZStack (inner)
.frame(width: geometry.size.width / 1.25, height: geometry.size.height / 10)
.opacity(self.isPresented ? 1 : 0)
} //ZStack (outer)
.padding(.bottom)
} //GeometryReader
} //body
} //Toast
With this you could toast Text, or an Image (or both, as shown below), or any other View.
here is the how to overlay on all of your views including NavigationView!
create a class model to store your views!
class ParentView:ObservableObject {
#Published var view:AnyView = AnyView(EmptyView())
}
create the model in your parrent view and call it in your view hierarchy
pass this class to your environment object of your parent view
struct Example: View {
#StateObject var parentView = ParentView()
var body: some View {
ZStack{
NavigationView{
ChildView()
.environmentObject(parentView)
.navigationTitle("dynamic parent view")
}
parentView.view
}
}
}
from now on you can call parentview in your child view by
#EnvironmentObject var parentView:ParentView
then for example in your tap gesture, you can change the parent view and show a pop up that covers everything including your navigationviews
#StateObject var parentView = ParentView()
here is the full solution copy and play with it in your preview!
import SwiftUI
class ParentView:ObservableObject {
#Published var view:AnyView = AnyView(EmptyView())
}
struct example: View {
#StateObject var parentView = ParentView()
var body: some View {
ZStack{
NavigationView{
ChildView()
.environmentObject(parentView)
.navigationTitle("dynamic parent view")
}
parentView.view
}
}
}
struct ChildView: View {
#EnvironmentObject var parentView:ParentView
var body: some View {
ZStack{
Text("hello")
.onTapGesture {
parentView.view = AnyView(Color.red.opacity(0.4).ignoresSafeArea())
}
}
}
}
struct example_Previews: PreviewProvider {
static var previews: some View {
example()
}
}
also you can improve this dramatically like this...!
struct ParentViewModifire:ViewModifier {
#EnvironmentObject var parentView:ParentView
#Binding var presented:Bool
let anyView:AnyView
func body(content: Content) -> some View {
content
.onChange(of: presented, perform: { value in
if value {
parentView.view = anyView
}
})
}
}
extension View {
func overlayAll<Overlay>(_ overlay: Overlay, presented: Binding<Bool>) -> some View where Overlay : View {
self
.modifier(ParentViewModifire(presented: presented, anyView: AnyView(overlay)))
}
}
now in your child view you can call this modifier on your view
struct ChildView: View {
#State var newItemPopUp:Bool = false
var body: some View {
ZStack{
Text("hello")
.overlayAll(newCardPopup, presented: $newItemPopUp)
}
}
}
App-wide View
If you want it to be app-wide, put in somewhere app-wide! For example, you can add it to the MyProjectApp.swift (or in sceneDelegate for UIKit/AppDelegate projects) file like this:
Note that the button and the State are just for more explanation and you may consider changing them in the way you like
#main
struct SwiftUIAppPlaygroundApp: App { // <- Note that where we are!
#State var showToast = false
var body: some Scene {
WindowGroup {
Button("App-Wide Button") { showToast.toggle() }
ZStack {
ContentView() // <- The app flow
if showToast {
MyCustomToastView().ignoresSafeArea(.all, edges: .all) // <- App-wide overlays
}
}
}
}
}
See? now you can add any sort of view on anywhere of the screen, without blocking animations. Just convert that #State to some sort of AppState like Observables or Environments and boom! 💥 you did it!
Note that it is a demo, you should use an environment variable or smt to be able for changing it from outside of this view's body
Apple does not currently provide any APIs that allow you to make global views similar to their own alert pop-ups.
In fact these views are actually still using UIKit under the hood.
If you want your own global pop-ups you can sort of hack your own (note this isn't tested, but something very similar should work for global presentation of toasts):
import SwiftUI
import Foundation
/// Global class that will manage toasts
class ToastPresenter: ObservableObject {
// This static property probably isn't even needed as you can inject via #EnvironmentObject
static let shared: ToastPresenter = ToastPresenter()
private init() {}
#Published private(set) var isPresented: Bool = false
private(set) var text: String?
private var timer: Timer?
/// Call this function to present toasts
func presentToast(text: String, duration: TimeInterval = 5) {
// reset the toast if one is currently being presented.
isPresented = false
self.text = nil
timer?.invalidate()
self.text = text
isPresented = true
timer = Timer(timeInterval: duration, repeats: false) { [weak self] _ in
self?.isPresented = false
}
}
}
/// The UI for a toast
struct Toast: View {
var text: String
var body: some View {
Text(text)
.padding()
.background(Capsule().fill(Color.gray))
.shadow(radius: 6)
.transition(AnyTransition.opacity.animation(.default))
}
}
extension View {
/// ViewModifier that will present a toast when its binding changes
#ViewBuilder func toast(presented: Binding<Bool>, text: String) -> some View {
ZStack {
self
if presented.wrappedValue {
Toast(text: text)
}
}
.ignoresSafeArea(.all, edges: .all)
}
}
/// The first view in your app's view hierarchy
struct RootView: View {
#StateObject var toastPresenter = ToastPresenter.shared
var body: some View {
MyAppMainView()
.toast(presented: $toastPresenter.isPresented, text: toastPresenter.text)
// Inject the toast presenter into the view hierarchy
.environmentObject(toastPresenter)
}
}
/// Some view later on in the app
struct SomeViewDeepInTheHierarchy: View {
#EnvironmentObject var toastPresenter: ToastPresenter
var body: some View {
Button {
toastPresenter.presentToast(text: "Hello World")
} label: {
Text("Show Toast")
}
}
}
Use .presentation() to show an alert when the button is tapped.
In LikeButton:
#Binding var liked: Bool
var body: some View {
Button(action: {self.liked = !self.liked}, label: {
Image(systemName: liked ? "heart.fill" : "heart")
}).presentation($liked) { () -> Alert in
Alert.init(title: Text("Thanks for liking!"))
}
}
You can also use .presentation() to present other Modal views, like a Popover or ActionSheet. See here and the "See Also" section on that page in Apple's SwiftUI documentation for info on the different .presentation() options.
Edit: Example of what you want with a custom view using Popover:
#State var liked = false
let popover = Popover(content: Text("Thanks for liking!").frame(width: 200, height: 100).background(Color.white), dismissHandler: {})
var body: some View {
Button(action: {self.liked = !self.liked}, label: {
Image(systemName: liked ? "heart.fill" : "heart")
}).presentation(liked ? popover : nil)
}
I am using this open source: https://github.com/huynguyencong/ToastSwiftUI . It is very simple to use.
struct ContentView: View {
#State private var isShowingToast = false
var body: some View {
VStack(spacing: 20) {
Button("Show toast") {
self.isShowingToast = true
}
Spacer()
}
.padding()
// Just add a modifier to show a toast, with binding variable to control
.toast(isPresenting: $isShowingToast, dismissType: .after(3)) {
ToastView(message: "Hello world!", icon: .info)
}
}
}