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

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

Related

Getting Values from own PickerView

I'm new to Swift and I'm currently developing my own Timer Application for practice purposes. (I do it without storyboard)
Now I have the Problem that i made a View called "TimePickerView" (Code below), where I created my own Picker. Then I use that TimePickerView in another part of my Application with other Views (in a View). In that View I want to pick my time but I don't know how i can get the Values of the Picker (The Picker works by the way)
This is my TimePickerView
import SwiftUI
struct TimePickerView: View {
#State private var selectedTimeIndexSecond = 0
#State private var selectedTimeIndexMinutes = 0
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(minutes[selectedTimeIndexMinutes]) min : \(seconds[selectedTimeIndexSecond]) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
func getValues() -> (Int, Int) {
return (self.minutes[selectedTimeIndexMinutes] ,self.seconds[selectedTimeIndexSecond])
}
}
and that is the View I want to use my Picker, but I don't know how I get those values from the Picker:
struct SetTimerView: View {
#State var timepicker = TimePickerView()
var body: some View {
NavigationView{
VStack{
//Select the time
timepicker
//Timer variables (This doesn't work)
var timerTime = timepicker.getValues()
var minutes = timerTime.0
var seconds = timerTime.1
Spacer()
let valid : Bool = isValid(timerTime: minutes+seconds)
//Confirm the time
NavigationLink(
destination:
getRightView(
validBool: valid,
timerTime: minutes*60 + seconds),
label: {
ConfirmButtonView(buttonText: "Confirm")
});
Spacer()
}
}
}
func isValid(timerTime : Int) -> Bool {
if (timerTime == 0) {
return false
} else {
return true
}
}
#ViewBuilder func getRightView(validBool : Bool, timerTime : Int) -> some View{
if (validBool == true) {
TimerView(userTime: CGFloat(timerTime), name: "David", isActive: true)
} else {
UnvalidTimeView()
}
}
}
I think main problem is misunderstanding conceptions between data and views.
At first you need a model witch will override your data (create it in separate swift file):
import Foundation
class Time: ObservableObject {
#Published var selectedTimeIndexMinutes: Int = 0
#Published var selectedTimeIndexSecond: Int = 0
}
Pay attention on ObservableObject so that swiftUI can easily detect changes to it that trigger any active views to redraw.
Next I try to change the value of the model in the view
import SwiftUI
struct TimePickerView: View {
#EnvironmentObject var timeData: Time
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $timeData.selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $timeData.selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(timeData.selectedTimeIndexMinutes) min : \(timeData.selectedTimeIndexSecond) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
}
struct TimePickerView_Previews: PreviewProvider {
static var previews: some View {
TimePickerView()
.environmentObject(Time())
}
}
Like you can see I don't using #Blinding, instead of it I connecting our Model with a View
On the next view I can see changes, I created a new one because your example have view that don't indicated here...
import SwiftUI
struct ReuseDataFromPicker: View {
#EnvironmentObject var timeData: Time
var body: some View {
VStack{
Text("You selected")
Text("\(timeData.selectedTimeIndexMinutes) min and \(timeData.selectedTimeIndexSecond) sec")
}
}
}
struct ReuseDataFromPicker_Previews: PreviewProvider {
static var previews: some View {
ReuseDataFromPicker()
.environmentObject(Time())
}
}
And collect all in a Content View
struct ContentView: View {
var body: some View {
TabView {
TimePickerView()
.tabItem {Label("Set Timer", systemImage: "clock.arrow.2.circlepath")}
ReuseDataFromPicker()
.tabItem {Label("Show Timer", systemImage: "hourglass")}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Time())
}
}
Like that you can easily change or reuse your data on any other views

List scroll freeze on catalyst NavigationView

I've run in to an odd problem with NavigationView on macCatalyst. Here below is a simple app with a sidebar and a detail view. Selecting an item on the sidebar shows a detail view with a scrollable list.
Everything works fine for the first NavigationLink, the detail view displays and is freely scrollable. However, if I select a list item which triggers a link to a second detail view, scrolling starts, then freezes. The app still works, only the detail view scrolling is locked up.
The same code works fine on an iPad without any freeze. If I build for macOS, the NavigationLink in the detail view is non-functional.
Are there any known workarounds ?
This is what it looks like, after clicking on LinkedView, a short scroll then the view freezes. It is still possible to click on the back button or another item on the sidebar, but the list view is blocked.
Here is the code:
ContentView.swift
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
var body: some View {
NavigationView {
List() {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailListView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...4).map({NamedItem(name: "\($0)")})
var body: some View {
VStack {
List {
Text(item.name)
NavigationLink(destination: DetailListView(item: NamedItem(name: "LinkedView"))) {
listItem(" LinkedView", "Item")
.foregroundColor(Color.blue)
}
ForEach(sections) { section in
sectionDetails(section)
}
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
TestListApp.swift
import SwiftUI
#main
struct TestListApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
I had this very same problem with Mac Catalyst app. On real device (iPhone 7 with iOS 14.4.2) there was no problem but with Mac Catalyst (MacBook Pro with Big Sur 11.2.3) the scrolling in the navigation view stuck very randomly as you explained. I figured out that the issue was with Macbook's trackpad and was related to scroll indicators because with external mouse the issue was absent. So the easiest solution to this problem is to hide vertical scroll indicators in navigation view. At least it worked for me. Below is some code from root view 'ContentView' how I did it. It's unfortunate to lose scroll indicators with big data but at least the scrolling works.
import SwiftUI
struct TestView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: NewView()) {
Text("Navigation Link to new view")
}
}
.onAppear {
UITableView.appearance().showsVerticalScrollIndicator = false
}
}
}
}
OK, so I managed to find a workaround, so thought I'd post this for help, until what seems to be a macCatalyst SwiftUI bug is fixed. I have posted a radar for the list freeze problem: FB8994665
The workaround is to use NavigationLink only to the first level of the series of pages which can be navigated (which gives me the sidebar and a toolbar), and from that point onwards use the NavigationStack package to mange links to other pages.
I ran in to a couple of other gotcha's with this arrangement.
Firstly the NavigationView toolbar loses its background when scrolling linked list views (unless the window is defocussed and refocussed), which seems to be another catalyst SwiftUI bug. I solved that by setting the toolbar background colour.
Second gotcha was that under macCatalyst the onTouch view modifier used in NavigationStack's PushView label did not work for most single clicks. It would only trigger consistently for double clicks. I fixed that by using a button to replace the label.
Here is the code, no more list freezes !
import SwiftUI
import NavigationStack
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
init() {
// Ensure toolbar is allways opaque
UINavigationBar.appearance().backgroundColor = UIColor.secondarySystemBackground
}
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
var body: some View {
NavigationStackView {
DetailListView(item: item)
}
}
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
var linked = NamedItem(name: "LinkedView")
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
List {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(item.name)
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: {
self.isSelected = linked.id
})
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
I have continued to experiment with NavigationStack and have made some modifications which will allow it to swap in and out List rows directly. This avoids the problems I was seeing with the NavigationBar background. The navigation bar is setup at the level above the NavigationStackView and changes to the title are passed via a PreferenceKey. The back button on the navigation bar hides if the stack is empty.
The following code makes use of PR#44 of swiftui-navigation-stack
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let depth: Int
let id = UUID()
init(name:String, depth: Int = 0) {
self.name = name
self.depth = depth
}
var linked: NamedItem {
return NamedItem(name: "Linked \(depth+1)", depth:depth+1)
}
}
// Preference Key to send title back down to DetailStackView
struct ListTitleKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
extension View {
func listTitle(_ title: String) -> some View {
self.preference(key: ListTitleKey.self, value: title)
}
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
#ObservedObject var navigationStack = NavigationStack()
#State var toolbarTitle: String = ""
var body: some View {
List {
NavigationStackView(noGroup: true, navigationStack: navigationStack) {
DetailListView(item: item, linked: item.linked)
.listTitle(item.name)
}
}
.listStyle(PlainListStyle())
.animation(nil)
// Updated title
.onPreferenceChange(ListTitleKey.self) { value in
toolbarTitle = value
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(toolbarTitle) \(self.navigationStack.depth)")
.toolbar(content: {
ToolbarItem(id: "BackB", placement: .navigationBarLeading, showsByDefault: self.navigationStack.depth > 0) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
.opacity(self.navigationStack.depth > 0 ? 1.0 : 0.0)
}
})
}
}
struct DetailListView: View {
var item: NamedItem
var linked: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked, linked: linked.linked)
.listTitle(linked.name)
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func buttonAction() {
self.isSelected = linked.id
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: buttonAction)
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}

Creating BaseView class in SwiftUI

Lately started learning/developing apps with SwiftUI and seems pretty easy to build the UI components. However, struggling creating a BaseView in SwiftUI. My idea is to have the common UI controls like background , navigation , etc in BaseView and just subclass other SwiftUI views to have the base components automatically.
Usually you want to either have a common behaviour or a common style.
1) To have a common behaviour: composition with generics
Let's say we need to create a BgView which is a View with a full screen image as background. We want to reuse BgView whenever we want. You can design this situation this way:
struct BgView<Content>: View where Content: View {
private let bgImage = Image.init(systemName: "m.circle.fill")
let content: Content
var body : some View {
ZStack {
bgImage
.resizable()
.opacity(0.2)
content
}
}
}
You can use BgView wherever you need it and you can pass it all the content you want.
//1
struct ContentView: View {
var body: some View {
BgView(content: Text("Hello!"))
}
}
//2
struct ContentView: View {
var body: some View {
BgView(content:
VStack {
Text("Hello!")
Button(action: {
print("Clicked")
}) {
Text("Click me")
}
}
)
}
}
2) To have a common behaviour: composition with #ViewBuilder closures
This is probably the Apple preferred way to do things considering all the SwiftUI APIs. Let's try to design the example above in this different way
struct BgView<Content>: View where Content: View {
private let bgImage = Image.init(systemName: "m.circle.fill")
private let content: Content
public init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body : some View {
ZStack {
bgImage
.resizable()
.opacity(0.2)
content
}
}
}
struct ContentView: View {
var body: some View {
BgView {
Text("Hello!")
}
}
}
This way you can use BgView the same way you use a VStack or List or whatever.
3) To have a common style: create a view modifier
struct MyButtonStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.red)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
.shadow(radius: 3)
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Button(action: {
print("Button1 clicked")
}) {
Text("Button 1")
}
.modifier(MyButtonStyle())
Button(action: {
print("Button2 clicked")
}) {
Text("Button 2")
}
.modifier(MyButtonStyle())
Button(action: {
print("Button3 clicked")
}) {
Text("Button 3")
}
.modifier(MyButtonStyle())
}
}
}
These are just examples but usually you'll find yourself using one of the above design styles to do things.
EDIT: a very useful link about #functionBuilder (and therefore about #ViewBuilder) https://blog.vihan.org/swift-function-builders/
I got a idea about how to create a BaseView in SwiftUI for common usage in other screen
By the way
Step .1 create ViewModifier
struct BaseScene: ViewModifier {
/// Scene Title
var screenTitle: String
func body(content: Content) -> some View {
VStack {
HStack {
Spacer()
Text(screenTitle)
.font(.title)
.foregroundColor(.white)
Spacer()
}.padding()
.background(Color.blue.opacity(0.8))
content
}
}
}
Step .2 Use that ViewModifer in View
struct BaseSceneView: View {
var body: some View {
VStack {
Spacer()
Text("Home screen")
.font(.title)
Spacer()
}
.modifier(BaseScene(screenTitle: "Screen Title"))
}
}
struct BaseSceneView_Previews: PreviewProvider {
static var previews: some View {
Group {
BaseSceneView()
}
}
}
Your Output be like:

SwiftUI: Number pad layout

I'm building a custom number pad in Swift UI. I want the buttons to be arranged in a grid that fills the screen. The only way I've found to do this for far is to use GeometryReader but this seems like overkill for this simple task. Is there a better way to write this?
GeometryReader { geometry in
HStack(spacing: 0) {
ForEach([a, b, c]) { n in
Button(action: { self.add(n) }) { Text("\(n)") }
.frame(width: geometry.size.width/3)
}
}
}.frame(height: 80)
This results in this, which is how I want it to look. Just curious if there is a good way to do this without GeometryReader.
Yes, you can do this without GeometryReader. You just have to make each key view expandable. Then your HStack and VStack will take care of the rest.
A Button tightly wraps its label subview, so you need to make the label subview expandable. A Color is a View that expands to fill whatever space it's given, so let's use Color.clear as the button's label, and overlay the real Text label on the Color. I think we should define a KeyPadButton View for this:
struct KeyPadButton: View {
var key: String
var body: some View {
Button(action: { self.action(self.key) }) {
Color.clear
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.accentColor))
.overlay(Text(key))
}
}
enum ActionKey: EnvironmentKey {
static var defaultValue: (String) -> Void { { _ in } }
}
#Environment(\.keyPadButtonAction) var action: (String) -> Void
}
extension EnvironmentValues {
var keyPadButtonAction: (String) -> Void {
get { self[KeyPadButton.ActionKey.self] }
set { self[KeyPadButton.ActionKey.self] = newValue }
}
}
#if DEBUG
struct KeyPadButton_Previews: PreviewProvider {
static var previews: some View {
KeyPadButton(key: "8")
.padding()
.frame(width: 80, height: 80)
.previewLayout(.sizeThatFits)
}
}
#endif
I've defined an EnvironmentKey so that we can use the environment to pass the action callback from the higher-level keypad view to all the buttons.
KeyPadButton looks like this:
If you don't like the border, just remove that modifier.
Note that I've manually set the size of the button in the PreviewProvider. Since this view is expandable, it'll preview at the device size by default, and we don't need to see one giant button.
Now let's define a KeyPadRow view to lay out one row of buttons:
struct KeyPadRow: View {
var keys: [String]
var body: some View {
HStack {
ForEach(keys, id: \.self) { key in
KeyPadButton(key: key)
}
}
}
}
Now we can define the KeyPad view to lay out the entire keypad and provide the action for the buttons:
struct KeyPad: View {
#Binding var string: String
var body: some View {
VStack {
KeyPadRow(keys: ["1", "2", "3"])
KeyPadRow(keys: ["4", "5", "6"])
KeyPadRow(keys: ["7", "8", "9"])
KeyPadRow(keys: [".", "0", "⌫"])
}.environment(\.keyPadButtonAction, self.keyWasPressed(_:))
}
private func keyWasPressed(_ key: String) {
switch key {
case "." where string.contains("."): break
case "." where string == "0": string += key
case "⌫":
string.removeLast()
if string.isEmpty { string = "0" }
case _ where string == "0": string = key
default: string += key
}
}
}
Finally, let's define a ContentView that shows the string above the keypad:
struct ContentView : View {
var body: some View {
VStack {
HStack {
Spacer()
Text(string)
}.padding([.leading, .trailing])
Divider()
KeyPad(string: $string)
}
.font(.largeTitle)
.padding()
}
#State private var string = "0"
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
#endif
Here's the final result:

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