How can I know if a SwiftUI Button is enabled/disabled? - swift

There is no isEnabled property for a SwiftUI button. How can i tell if it is enabled?
In regular UIKit, i would simply do
if button.isEnabeld == true {
} else {
}
but there is no SwiftUI equivalent.

Inside a view, if you wish to react to the state set by .disabled(true), you can use:
#Environment(\.isEnabled) var isEnabled
Since the environment can be used from within a View or a ViewModifier, this can be used to change layout properties of a view based on the state set from outside.
Unfortunately, ButtonStyle cannot directly use #Environment, but you can use a ViewModifier to inject environment values into a ButtonStyle in order to use the value from within a ButtonStyle:
// First create a button style that gets the isEnabled value injected
struct MyButtonStyle: ButtonStyle {
private let isEnabled: Bool
init(isEnabled: Bool = true) {
self.isEnabled = isEnabled
}
func makeBody(configuration: Configuration) -> some View {
return configuration
.label
.background(isEnabled ? .green : .gray)
.foregroundColor(isEnabled ? .black : .white)
}
}
// Then make a ViewModifier to inject the state
struct MyButtonModifier: ViewModifier {
#Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
return content.buttonStyle(MyButtonStyle(isEnabled: isEnabled))
}
}
// Then create a convenience function to apply the modifier
extension Button {
func styled() -> some View {
ModifiedContent(content: self, modifier: MyButtonModifier())
}
}
// Finally, try out the button and watch it respond to it's state
struct ContentView: View {
var body: some View {
Button("Test", {}).styled().disabled(true)
}
}
You can use this method to inject other things into a ButtonStyle, like size category and theme.
I use it with a custom style enum that contains all the flavours of button styles found in our design system.

From outside a view you should know if you used .disabled(true) modifier.
From inside a view you can use #Environment(\.isEnabled) to get that information:
struct MyButton: View {
let action: () -> Void
#Environment(\.isEnabled) private var isEnabled
var body: some View {
Button(action: action) {
Text("Click")
}
.foregroundColor(isEnabled ? .green : .gray)
}
}
struct MyButton_Previews: PreviewProvider {
static var previews: some View {
VStack {
MyButton(action: {})
MyButton(action: {}).disabled(true)
}
}
}

The whole idea of SwiftUI, is to avoid duplication of the source of truth. You need to think differently, and consider where the source of truth is. This is where you need to go to find out the button's state. Not from the button itself.
In "Data Flow Through SwiftUI", at minute 30:50, they explain that every piece of data has a single source of truth. If your button gets its state from some #Binding, #State, #EnvironmentObject, etc, your if statement should get that information from the same place too, not from the button.

Short answer: Just use inside struct:
#Environment(\.isEnabled) private var isEnabled
Button style with:
animation on hover change
animation on disable/enable change
can be applied on any button in native way of swiftUI
you need manually set size of buttons outside of the button
usage:
#State var isDisabled = false
///.......
Button("Styled button") { isDisabled.toggle() }
.buttonStyle(ButtStyle.BigButton()) // magic inside
.frame(width: 200, height: 50)
.disabled(isDisabled)
Button("switch isDisabled") { isDisabled.toggle() }
source code:
public struct ButtStyle { }
// Added style to easy stylyng in native way for SwiftUI
#available(macOS 11.0, *)
public extension ButtStyle {
struct BigButton: ButtonStyle {
init() {
}
public func makeBody(configuration: Configuration) -> some View {
BigButtonStyleView(configuration: configuration)
}
}
}
#available(macOS 11.0, *)
struct BigButtonStyleView : View {
let configuration: ButtonStyle.Configuration
#Environment(\.isEnabled) var isEnabled // here we getting "disabled"
#State var hover : Bool = false
var body: some View {
// added animations
MainFrameMod()
.animation(.easeInOut(duration: 0.2), value: hover)
.animation(.easeInOut(duration: 0.2), value: isEnabled)
}
// added opacity on move hover change
// and disabled status
#ViewBuilder
func MainFrameMod() -> some View {
if isEnabled {
MainFrame()
.opacity(hover ? 1 : 0.8)
.onHover{ hover = $0 }
} else {
MainFrame()
.opacity(0.5)
}
}
// Main interface of button
func MainFrame() -> some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color(hex: 0xD8D8D8))
configuration.label
.foregroundColor(.black)
.font(.custom("SF Pro", size: 18))
}
}
}

As mentioned by other developers, the main idea of SwiftUI is that the UI remains synced with the data. You can perform this in many different ways. This includes #State, #EnvironmentObject, #Binding etc.
struct ContentView: View {
#State private var isEnabled: Bool = false
var body: some View {
VStack {
Button("Press me!") {
}.disabled(isEnabled)
}
.padding()
}
}

Related

Why swift view does not update with UIKit?

I have swiftUI tabbar with animation also I have UITabbarViewController which contains this swiftUI view.
SwiftUI
struct MainTabBarView: View {
#ObservedObject var viewModel: MainTabBarViewModel
var body: some View {
VStack {
HStack(spacing: 0) {
TabItem(currentIndex: $viewModel.index, tabIndex: 0, tab: .home)
Spacer()
TabItem(currentIndex: $viewModel.index, tabIndex: 1, tab: .search)
Spacer()
TabItem(currentIndex: $viewModel.index, tabIndex: 2, tab: .library)
}.padding(.top, 8).padding(.leading, 28).padding(.trailing, 28)
.padding(.bottom, 8)
.frame(width: UIScreen.main.bounds.width)
.animation(.easeIn(duration: 0.2))
}
UITabbarViewController:
final class MainTabBarViewController: UITabBarController, Navigatable {
private var viewModel = MainTabBarViewModel()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
override func viewDidLoad() {
super.viewDidLoad()
configTabBarView()
}
private func configTabBarView() {
let view = UIHostingController(rootView: MainTabBarView(viewModel: viewModel))
addChild(child)
tabBar.addSubview(view.view)
view.didMove(toParent: self)
tabBar.setValue(true, forKey: "hidesShadow")
view.view.snp.makeConstraints { make in
make.leading.trailing.top.equalToSuperview()
make.height.equalTo(100)
}
}
ViewModel:
final class MainTabBarViewModel: NSObject, ObservableObject {
#Published var index: Int = 0
}
This is the code of my custom view with tabs.
When I press on tab -> index from viewModel is changing in this view and then
struct TabItem: View {
#Binding var currentIndex: Int
var tabIndex: Int
var isCurrentTab: Bool {
return currentIndex == tabIndex
}
var tab: Tabs
var body: some View {
HStack {
Image(uiImage: tab.icon).resizable().frame(width: 25, height: 25, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.foregroundColor(isCurrentTab ? R.color.primaryColor()!.swiftUI : R.color.grayScaleDark()!.swiftUI)
Text(isCurrentTab ? tab.title : "").font(Font(R.font.poppinsRegular(size: 16)! as CTFont))
.foregroundColor(isCurrentTab ? R.color.primaryColor()!.swiftUI : R.color.grayScaleDark()!.swiftUI)
}.padding(15)
.onTapGesture {
self.currentIndex = tabIndex
logWarn(String(tabIndex))
}
.background(self.tabIndex == currentIndex ? Color(R.color.pink()!) : Color.white)
.frame(height: 44.0)
.cornerRadius(22.0)
.clipped()
}
}
The problems that index is changing, but tab does not update view. It is look, like view does not update.
Although it would be helpful for the given code to compile, I think I've pieced together a solution that allows the current index to remain in MainTabBarViewModel, under the assumption that the it is intended to hold other things in the future. I also make the assumption that TabItem is supposed to be sufficiently generic that it doesn't have to depend on MainTabVarViewModel specifically.
My idea is based on providing a key path to the current index when creating the TabItem. Were it not for the fact that TabItem updates the current index that would be simple enough. However, because it does update it, in a onTapGesture closure, the compiler complains that it can't write through the KeyPath, because self is immutable. So... summoning the ghost a David Wheeler, I tried doing it through a closure saved in TabItem.init... which doesn't exist, so adding that is part of the solution.
First TabItem becomes a generic:
struct TabItem<TabBarViewModel: ObservableObject>: View {
#ObservedObject var viewModel: TabBarViewModel
let indexKeyPath: WritableKeyPath<TabBarViewModel, Int>
let tapClosure: (Int) -> Void
// #Binding var currentIndex: Int
var currentIndex: Int { viewModel[keyPath: indexKeyPath] }
...
init(currentIndexIn viewModel: TabBarViewModel, at indexKeyPath: WritableKeyPath<TabBarViewModel, Int>, tabIndex: Int, tab: Tabs)
{
self.viewModel = viewModel
self.indexKeyPath = indexKeyPath
self.tabIndex = tabIndex
self.tab = tab
self.tapClosure = { self.viewModel[keyPath: indexKeyPath] = $0 }
}
With that everything in TabItem compiles (after removing references to unprovided code) except for this bit in body
.onTapGesture {
currentIndex = tabIndex
print(String(tabIndex))
}
I change that to use the closure that was saved off in the init:
.onTapGesture {
tapClosure(tabIndex)
print(String(tabIndex))
}
Then in MainTabBarView's body creating a TabItem changes from this:
TabItem(currentIndex: $viewModel.index, tabIndex: 0, tab: .home)
to this
TabItem(currentIndexIn: viewModel, at: \.index, tabIndex: 0, tab: .home)
Since there is too much code missing for me to compile and test it, this is a bit of a guess. I stubbed out missing code just to silence the errors for it, so I can say that at least this solution compiles. It would need to be applied and tested in the actual app to verify whether it works.
Anyway, assuming it does work, this solution still allows keeping the index in an observable object without relying on the particular type of that object.

Undo/redo text input w/ SwiftUI TextEditor

Admittedly this is a broad question, but is it possible to undo or redo text input (via iOS's UndoManager?) when using a SwiftUI TextEditor control? I've looked everywhere and was unable to find any resource focusing on this workflow combination (SwiftUI + TextEditor + UndoManager). I'm wondering given the relative immaturity of TextEditor that either this isn't possible at all, or requires some plumbing work to facilitate. Any guidance will be greatly appreciated!
Admittedly, this is a bit of a hack and non very SwiftUI-y, but it does work. Basically declare a binding in your UITextView:UIViewRepresentable to an UndoManager. Your UIViewRepresentable will set that binding to the UndoManager provided by the UITextView. Then your parent View has access to the internal UndoManager. Here's some sample code. Redo works as well although not shown here.
struct MyTextView: UIViewRepresentable {
/// The underlying UITextView. This is a binding so that a parent view can access it. You do not assign this value. It is created automatically.
#Binding var undoManager: UndoManager?
func makeUIView(context: Context) -> UITextView {
let uiTextView = UITextView()
// Expose the UndoManager to the caller. This is performed asynchronously to avoid modifying the view at an inappropriate time.
DispatchQueue.main.async {
undoManager = uiTextView.undoManager
}
return uiTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
struct ContentView: View {
/// The underlying UndoManager. Even though it looks like we are creating one here, ultimately, MyTextView will set it to its internal UndoManager.
#State private var undoManager: UndoManager? = UndoManager()
var body: some View {
NavigationView {
MyTextView(undoManager: $undoManager)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
undoManager?.undo()
} label: {
Image(systemName: "arrow.uturn.left.circle")
}
Button {
undoManager?.redo()
} label: {
Image(systemName: "arrow.uturn.right.circle")
}
}
}
}
}
}
In respect to using UIViewRepresentable as a TextView or TextField…. this approach works for undo, but not for redo it seems.
The redo button condition undoManager.canRedo seems to change appropriately. However, it doesn’t return any undone text into either the textfield or TextView
I’m now wondering is this a bug or something I’m missing in the logic?
import SwiftUI
import PlaygroundSupport
class Model: ObservableObject {
#Published var active = ""
func registerUndo(_ newValue: String, in undoManager: UndoManager?) {
let oldValue = active
undoManager?.registerUndo(withTarget: self) { target in
target.active = oldValue
}
active = newValue
}
}
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
struct ContentView: View {
#ObservedObject private var model = Model()
#Environment(\.undoManager) var undoManager
#State var text: String = ""
var body: some View {
ZStack (alignment: .bottomTrailing) {
// Testing TextView for undo & redo functionality
TextView(text: Binding<String>(
get: { self.model.active },
set: { self.model.registerUndo($0, in: self.undoManager) }))
HStack{
// Testing TextField for undo & redo functionality
TextField("Enter Text...", text: Binding<String>(
get: { self.model.active },
set: { self.model.registerUndo($0, in: self.undoManager) })).padding()
Button("Undo") {
withAnimation {
self.undoManager?.undo()
}
}.disabled(!(undoManager?.canUndo ?? false)).padding()
Button("Redo") {
withAnimation {
self.undoManager?.redo()
}
}.disabled(!(undoManager?.canRedo ?? false)).padding()
}.background(Color(UIColor.init(displayP3Red: 0.1, green: 0.3, blue: 0.3, alpha: 0.3)))
}.frame(width: 400, height: 400, alignment: .center).border(Color.black)
}
}
PlaygroundPage.current.setLiveView(ContentView())

How to use SwiftUI and Combine to propagate state changes of child View to parent View?

How can I, using SwiftUI and Combine, have a state of the uppermost View depend on a state of its contained SubView, determined by criteria among others dependent on its contained SubSubView?
The scenario
I have the following View hierarchy: V1 contains V2, which contains V3.
V1 is a general, mostly decorative, 'wrapper' of a specific settings view V2 and holds a "Save" button. The button's disabled state of type Bool should depend on the save-ability state of V2.
V2 is a specific settings view. Which type of V2, the specific settings shown, may differ depending on the rest of my program. It is guaranteed to be able to determine its save-ability. It contains a Toggle and V3, a MusicPicker. V2's save-ability is dependent on criteria processing V3's selection-state and its Toggle-state.
V3 is a general 'MusicPicker' view with a selection-state of type Int?. It could be used with any parent, communicating bidirectionally its selection-state.
A Binding should normally be used to communicate back and forth between 2 views. As such, there could be a binding between V1 and V2 and V2 and V3. However, V2 cannot/should not react to a binding's value change of V3 and communicate this (along with other criteria) back to V1, as far as I know/understand. I may use ObservableObjects to share a save-ability with V1 and V2 and to share a selection-state with V2 and V3, but it is unclear to me how to integrate V3's ObservableObject changes with other criteria to set V1's ObservableObject.
The examples
Using #State and #Binding
/* V1 */
struct SettingsView: View {
#State var saveable = false
var body: some View {
VStack {
Button(action: saveAction){
Text("Save")
}.disabled(!saveable)
getSpecificV2(saveable: $saveable)
}
}
func getSpecificV2(saveable: Binding<Bool>) -> AnyView {
// [Determining logic...]
return AnyView(SpecificSettingsView(saveable: saveable))
}
func saveAction(){
// More code...
}
}
/* V2 */
struct SpecificSettingsView: View {
#Binding var saveable: Bool
#State var toggled = false
#State var selectedValue: Int?
var body: some View {
Form {
Toggle("Toggle me", isOn: $toggled)
CustomPicker(selected: $selectedValue)
}
}
func someCriteriaProcess() -> Bool {
if let selected = selectedValue {
return (selected == 5)
} else {
return toggled
}
}
}
/* V3 */
struct CustomPicker: View {
#Binding var selected: Int?
var body: some View {
List {
Text("None")
.onTapGesture {
self.selected = nil
}.foregroundColor(selected == nil ? .blue : .primary)
Text("One")
.onTapGesture {
self.selected = 1
}.foregroundColor(selected == 1 ? .blue : .primary)
Text("Two")
.onTapGesture {
self.selected = 2
}.foregroundColor(selected == 2 ? .blue : .primary)
}
}
}
In this example code, I would need to essentially have saveable be dependent on someCriteriaProcess().
Using ObservableObject
In response to Tobias' answer, a possible alternative would be to use ObservableObjects.
/* V1 */
class SettingsStore: ObservableObject {
#Published var saveable = false
}
struct SettingsView: View {
#ObservedObject var store = SettingsStore()
var body: some View {
VStack {
Button(action: saveAction){
Text("Save")
}.disabled(!store.saveable)
getSpecificV2()
}.environmentObject(store)
}
func getSpecificV2() -> AnyView {
// [Determining logic...]
return AnyView(SpecificSettingsView())
}
func saveAction(){
// More code...
}
}
/* V2 */
struct SpecificSettingsView: View {
#EnvironmentObject var settingsStore: SettingsStore
#ObservedObject var pickerStore = PickerStore()
#State var toggled = false
#State var selectedValue: Int?
var body: some View {
Form {
Toggle("Toggle me", isOn: $toggled)
CustomPicker(store: pickerStore)
}.onReceive(pickerStore.objectWillChange){ selected in
print("Called for selected: \(selected ?? -1)")
self.settingsStore.saveable = self.someCriteriaProcess()
}
}
func someCriteriaProcess() -> Bool {
if let selected = selectedValue {
return (selected == 5)
} else {
return toggled
}
}
}
/* V3 */
class PickerStore: ObservableObject {
public let objectWillChange = PassthroughSubject<Int?, Never>()
var selected: Int? {
willSet {
objectWillChange.send(newValue)
}
}
}
struct CustomPicker: View {
#ObservedObject var store: PickerStore
var body: some View {
List {
Text("None")
.onTapGesture {
self.store.selected = nil
}.foregroundColor(store.selected == nil ? .blue : .primary)
Text("One")
.onTapGesture {
self.store.selected = 1
}.foregroundColor(store.selected == 1 ? .blue : .primary)
Text("Two")
.onTapGesture {
self.store.selected = 2
}.foregroundColor(store.selected == 2 ? .blue : .primary)
}
}
}
Using the onReceive() attachment, I try to react to any changes of the PickerStore. Although the action fires and the debug prints correctly, no UI change is shown.
The question
What is (in this scenario) the most appropriate approach to react to a change in V3, process this with other states in V2, and correspondingly change a state of V1, using SwiftUI and Combine?
Posting this answer on the premise of the approach with ObservableObject that is added on your question itself.
Look carefully. As soon as the code:
.onReceive(pickerStore.objectWillChange){ selected in
print("Called for selected: \(selected ?? -1)")
self.settingsStore.saveable = self.someCriteriaProcess()
}
runs in the SpecificSettingsView the settingsStore is about to change which triggers the parent SettingsView to refresh its associated view components. That means the func getSpecificV2() -> AnyView will return SpecificSettingsView object that in turns will instantiate the PickerStore again. Because,
SwiftUI views, being value type (as they are struct), will not retain your objects within their view scope if the view is recreated by a parent view, for example. So it’s best to pass those observable objects by reference and have a sort of container view, or holder class, which will instantiate and reference those objects. If the view is the only owner of this object, and that view is recreated because its parent view is updated by SwiftUI, you’ll lose the current state of your ObservedObject.
(Read More on the above)
If you just push the instantiation of the PickerStore higher in the view hierarchy (probably the ultimate parent) you will get the expected behavior.
struct SettingsView: View {
#ObservedObject var store = SettingsStore()
#ObservedObject var pickerStore = PickerStore()
. . .
func getSpecificV2() -> AnyView {
// [Determining logic...]
return AnyView(SpecificSettingsView(pickerStore: pickerStore))
}
. . .
}
struct SpecificSettingsView: View {
#EnvironmentObject var settingsStore: SettingsStore
#ObservedObject var pickerStore: PickerStore
. . .
}
Note: I uploaded the project at remote repository here
Because SwiftUI doesn't support refreshing Views on changes inside a nested ObservableObject, you need to do this manually. I posted a solution here on how to do this:
https://stackoverflow.com/a/58996712/12378791 (e.g. with ObservedObject)
https://stackoverflow.com/a/58878219/12378791 (e.g. with EnvironmentObject)
I have figured out a working approach with the same end result, that may be useful to others. It does not, however, pass data in the way I requested in my question, but SwiftUI does not seem suitable to do so in any case.
As V2, the 'middle' view, can properly access both important states, that of the selection and save-ability, I realised I could make V2 the parent view and have V1, initially the 'parent' view, be a child view accepting #ViewBuilder content instead. This example would not be applicable to all cases, but it would to mine.
A working example is as follows.
/* V2 */
struct SpecificSettingsView: View {
#State var toggled = false
#State var selected: Int?
var saveable: Bool {
return someCriteriaProcess()
}
var body: some View {
SettingsView(isSaveable: self.saveable, onSave: saveAction){
Form {
Toggle("Toggle me", isOn: self.$toggled)
CustomPicker(selected: self.$selected)
}
}
}
func someCriteriaProcess() -> Bool {
if let selected = selected {
return (selected == 2)
} else {
return toggled
}
}
func saveAction(){
guard saveable else { return }
// More code...
}
}
/* V1 */
struct SettingsView<Content>: View where Content: View {
var content: () -> Content
var saveAction: () -> Void
var saveable: Bool
init(isSaveable saveable: Bool, onSave saveAction: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content){
self.saveable = saveable
self.saveAction = saveAction
self.content = content
}
var body: some View {
VStack {
// More decoration
Button(action: saveAction){
Text("Save")
}.disabled(!saveable)
content()
}
}
}
/* V3 */
struct CustomPicker: View {
#Binding var selected: Int?
var body: some View {
List {
Text("None")
.onTapGesture {
self.selected = nil
}.foregroundColor(selected == nil ? .blue : .primary)
Text("One")
.onTapGesture {
self.selected = 1
}.foregroundColor(selected == 1 ? .blue : .primary)
Text("Two")
.onTapGesture {
self.selected = 2
}.foregroundColor(selected == 2 ? .blue : .primary)
}
}
}
I hope this proves useful to others.

SwiftUI: Change #State variable through a function called externally?

So maybe I'm misunderstanding how SwiftUI works, but I've been trying to do this for over an hour and still can't figure it out.
struct ContentView: View, AKMIDIListener {
#State var keyOn: Bool = false
var key: Rectangle = Rectangle()
var body: some View {
VStack() {
Text("Foo")
key
.fill(keyOn ? Color.red : Color.white)
.frame(width: 30, height: 60)
}
.frame(width: 400, height: 400, alignment: .center)
}
func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel, portID: MIDIUniqueID? = nil, offset: MIDITimeStamp = 0) {
print("foo")
keyOn.toggle()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
So the idea is really simple. I have an external midi keyboard using AudioKit. When a key on the keyboard is pressed, the rectangle should change from white to red.
The receivedMIDINoteOn function is being called and 'foo' is printed to the console, and despite keyOn.toggle() appearing in the same function, this still won't work.
What's the proper way to do this?
Thanks
Yes, you are thinking of it slightly wrong. #State is typically for internal state changes. Have a button that your View directly references? Use #State. #Binding should be used when you don't (or shouldn't, at least) own the state. Typically, I use this when I have a parent view who should be influencing or be influenced by a subview.
But what you are likely looking for, is #ObservedObject. This allows an external object to publish changes and your View subscribes to those changes. So if you have some midi listening object, make it an ObservableObject.
final class MidiListener: ObservableObject, AKMIDIListener {
// 66 key keyboard, for example
#Published var pressedKeys: [Bool] = Array(repeating: false, count: 66)
init() {
// set up whatever private storage/delegation you need here
}
func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel, portID: MIDIUniqueID? = nil, offset: MIDITimeStamp = 0) {
// how you determine which key(s) should be pressed is up to you. if this is monophonic the following will suffice while if it's poly, you'll need to do more work
DispatchQueue.main.async {
self.pressedKeys[Int(noteNumber)] = true
}
}
}
Now in your view:
struct KeyboardView: View {
#ObservedObject private var viewModel = MidiListener()
var body: some View {
HStack {
ForEach(0..<viewModel.pressedKeys.count) { index in
Rectangle().fill(viewModel.pressedKeys[index] ? Color.red : Color.white)
}
}
}
}
But what would be even better is to wrap your listening in a custom Combine.Publisher that posts these events. I will leave that as a separate question, how to do that.

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