I have created a ViewModifier to show alert with 2 buttons. How to inform my ContentView that OK button is clicked, so that I can perform button action?
Sample code: ShowAlert is my custom ViewModifier
struct ShowAlert: ViewModifier {
#Binding var showingAlert: Bool
let title: String
let message: String
func body(content: Content) -> some View {
content
.alert(isPresented: $showingAlert) { () -> Alert in
Alert(title: Text(title), message: Text(message),
primaryButton: .default (Text("OK")) {
print("OK button tapped")
//How to trigger ok button clicked event to my content view
},secondaryButton: .cancel())
}
}
}
View Implementation
ScrollView {
....
}.navigationBarTitle("Click")
.navigationBarItems(trailing: Button(action: {
self.showAlert = true
}) {
Image(systemName: "lock")
}.modifier(ShowAlert(showingAlert: $showAlert, title: "", message: "Are you sure you want to Logout"))
Here is a demo of solution with passed callback into modifier. Tested with Xcode 11.4 / iOS 13.4.
struct ShowAlert: ViewModifier {
#Binding var showingAlert: Bool
let title: String
let message: String
var callback: () -> () = {} // << here !!
func body(content: Content) -> some View {
content
.alert(isPresented: $showingAlert) { () -> Alert in
Alert(title: Text(title), message: Text(message),
primaryButton: .default (Text("OK")) {
print("OK button tapped")
self.callback() // << here !!
},secondaryButton: .cancel())
}
}
}
// Demo view
struct TestAlertModifier: View {
#State private var showAlert = false
#State private var demoLog = "Wait for alert..."
var body: some View {
NavigationView {
ScrollView {
Text(demoLog)
}.navigationBarTitle("Click")
.navigationBarItems(trailing: Button(action: {
self.showAlert = true
}) {
Image(systemName: "lock")
}.modifier(ShowAlert(showingAlert: $showAlert, title: "",
message: "Are you sure you want to Logout", callback: confirmAlert)))
}
}
private func confirmAlert() {
self.demoLog = "Tapped - OK"
}
}
Related
Menu{
Button("Profile", action: {})
Button("Settings", action: {})
Button(action: {
self.showingAlert = true
}, label: {
Text("Logout")
})
} label: {
Button(action: {
}) {
Image( "icon-menu").imageScale(.large)
}
}.alert(isPresented:$showingAlert){
Alert(title: Text("Logout?"), message: Text("Are you sure you want to logout?"), primaryButton: .default(Text("Ok"), action: {
}), secondaryButton: .cancel())
}
Alert is not showing on the click of logout. Can someone help on this
I need to show an alert on the click of a menu item. But it is not working
your code works well for me. This is the code I used for testing, on real devices ios 16.3 and macCatalyst with macos 13.2
struct ContentView: View {
#State var showingAlert = false
var body: some View {
Menu {
Button("Profile", action: {})
Button("Settings", action: {})
Button(action: { showingAlert = true }, label: {
Text("Logout")
})
} label: {
Button(action: { }) {
Image(systemName: "ellipsis.circle").imageScale(.large)
}
}.alert(isPresented:$showingAlert){
Alert(title: Text("Logout?"),
message: Text("Are you sure you want to logout?"),
primaryButton: .default(Text("Ok"), action: { }),
secondaryButton: .cancel())
}
}
}
Create one function for logout action
struct ContentView: View {
var body: some View {
Menu{
Button("Profile", action: {})
Button("Settings", action: {})
Button("Logout", action: logoutAction)
}
}
func logoutAction() {
}}
Add your alert related call inside logoutAction method
I am creating a custom alert reusable modifier which will have different number of buttons depending upon the style I need. Anyways in my sample code below, I have removed as much extra code as I can to keep it small and simple.
With the code below, I am able to show the alert, but clicking on "Click Me" button does not dismiss the alert. I feel something is wrong with the way I am setting "isPresented" binding variable, but not able to figure out what. Am new to SwiftUI.
Custom Alert Modifier:
struct CustomAlertView: ViewModifier {
var isPresented: Binding<Bool>
init(isPresented: Binding<Bool>) {
self.isPresented = isPresented
}
func body(content: Content) -> some View {
content.overlay(alertContent())
}
#ViewBuilder
private func alertContent() -> some View {
GeometryReader { geometry in
if self.isPresented {
// Do something
// Setting isPresented to false is not doing anything when button is clicked.
Button(action: { self.isPresented.wrappedValue = false }) {
Text("Click Me")
}
}
}
}
}
func customAlert(isPresented: Binding<Bool>) -> some View {
return modifier(CustomAlertView(isPresented: isPresented))
}
View and view model code:
struct MyView: View {
#ObservedObject var viewModel: CustomViewModel = CustomViewModel()
var body: some View {
VStack {
switch viewModel.state {
case .idle:
Color.clear.onAppear(perform: { viewModel.doSomething() })
case .showNewView:
// Navigate
}
}.onAppear() {
viewModel.state = .idle
}.customAlert(isPresented: .constant(viewModel.showAlert))
}
}
#MainActor
class CustomViewModel: ObservableObject {
enum State {
case idle
case showNewView
}
#Published var state = State.idle
#Published var showAlert = false
func doSomething() {
if failure {
self.showAlert = true
} else {
self.state = .showNewView
}
}
}
What am I doing wrong or why am I not able to dismiss the alert?
Thanks for looking!
First of all, you want #Binding var isPresented: Bool inside the CustomAlertView, and you assign it in init as self._isPresented = isPresented:
struct CustomAlertView: ViewModifier {
#Binding var isPresented: Bool // <-- like this
init(isPresented: Binding<Bool>) {
self._isPresented = isPresented // <-- and like this
}
//...
And second, you definitely don't want to set isPresented to a constant in .customAlert(isPresented: .constant(viewModel.showAlert)). Instead, you need to pass the binding variable:
VStack {
// ...
}
.customAlert(isPresented: $viewModel.showAlert) // <-- bind it here
I'm trying to trigger an alert when is an error in the model but it never get updated to show the alert:
Here is my implementation in the view:
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
#State var showAlert = false
init() {
viewModel = ViewModel()
showAlert = viewModel.showAlert
}
var body: some View {
NavigationView {
Text("Hello, world!")
.padding()
}
.alert(isPresented: $showAlert) {
Alert(title: Text("This works"),
message: Text("Hello"),
dismissButton: .default(Text("got it"))
)}
}
}
Here is my models:
class ViewModel: ObservableObject {
#Published var showAlert = false
var cancellables = Set<AnyCancellable>()
init() {
DoSomething.shared.showAlert.sink { _ in
print("got new Value")
} receiveValue: {[weak self] value in
print("value")
self?.showAlert = value
}.store(in: &cancellables)
}
}
class DoSomething {
let showAlert = PassthroughSubject<Bool, Never>()
static let shared = DoSomething()
private init() {
checkToShowAlert()
}
func checkToShowAlert() {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
print("change value")
self?.showAlert.send(true)
}
}
}
Any of you knows why the showAlert variable it never gets updated?
I'll really appreciate your help
In your current code, you're setting ContentView's showAlert to the ViewModel's showAlert at that point in time:
init() {
viewModel = ViewModel()
showAlert = viewModel.showAlert //<-- assignment at the time of init
}
Meaning, it's false at the time of assignment. Because it's just a Bool getting assigned to another Bool, there's no mechanism to keep it updated if ViewModel's showAlert changes.
The simplest solution is to get rid of your #State variable and observe the #Published property directly:
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel()
var body: some View {
NavigationView {
Text("Hello, world!")
.padding()
}
.alert(isPresented: $viewModel.showAlert) {
Alert(title: Text("This works"),
message: Text("Hello"),
dismissButton: .default(Text("got it"))
)}
}
}
Best remove the view model object, we don't need those in SwiftUI because the View struct holds the view data and the #State and #Binding property wrappers make the struct behave like an object.
Also, I don't think you need Combine for what you are trying to do because you aren't combining anything using combineLatest etc, but when we do use it in SwiftUI we don't use sink or store, instead we assign the end of the pipeline to an #Published.
Example:
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
VStack{
Text("Hello, world!")
.padding()
Button("Show Alert") {
showAlert()
}
}
}
.alert(isPresented: $isPresented) {
Alert(title:Text("This works"),
message: Text("Hello"),
dismissButton: .default(Text("got it"))
)}
}
func showAlert() {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
isPresented = true
}
}
}
The general structure of my code is that I have a UIKit app in which I am trying to embed a swiftui view. So I have a file called SettingsViewController which is as follows:
class SettingsViewController: UIViewController {
...
var items: [(SettingsView.Setting)] = ...
var actionsForItems: [() -> Void = []
#State var isOn: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
actionsForItems = ...
...
addSettingCell(isOn: isOn)
let childView = UIHostingController(rootView: SettingsView(settings: items, actionsForSettings: actionsForItems))
addChild(childView)
childView.view.frame = container.bounds
container.addSubview(childView.view)
childView.didMove(toParent: self)
}
...
func addCell(isOn: Bool) {
items.insert((settingName, $isOn as AnyObject) as SettingsView.Setting)
actionsForItems.insert({
self.onSwitchValueChanged(isOn: isOn) //defined later
})
}
}
which creates a view called Settings View which is structured as follows:
struct SettingsView: View {
typealias Setting = (key: String, value: AnyObject?)
var settings: [Setting]
var actions: [() -> Void]
var body: some View {
List(settings.indices) { index in
...
SettingsWithSwitchView(setting: settings[index], action: actions[index], isOn: setting.value as Binding<Bool>)
}
Spacer()
}
}
and SettingsWithSwitchView is as follows:
struct SettingsWithSwitchView: View {
var setting: SettingsView.Setting
var action: () -> Void
#Binding var isOn: Bool {
willSet {
print("willSet: newValue =\(newValue) oldValue =\(isOn)")
}
didSet {
print("didSet: oldValue=\(oldValue) newValue=\(isOn)")
action()
}
}
var body: some View {
HStack {
Text(setting.key)
.foregroundColor(Color("GrayText"))
.font(Font.custom("OpenSans", size: 15))
Spacer()
Toggle(isOn: $isOn) {}
}
}
}
I read in another post here on Stack Overflow that calling didSet on the isOn property should be the way to accomplish this, but I need to call onSwitchValueChanged when the Toggle value is updated, but my current setup does not work. I would really appreciate some help to figure out how to do this. I can update with some other information if necessary.
The thing that ended up working for me was creating a ViewModel which was also an ObservableObject and then setting the action for the toggle inside of .onTapGesture
I'm developing a simple iOS app with SwiftUI with two views: a LogInView() and a HomeView().
What I want is really simple: when the user clicks on the Login Button on LogInView() I want the app to hide the LogInView() and show the HomeView(), full screen, not like a modal and without allowing the user to go back.
This could be easily done with Storyboards in Swift and UIKit, is there any way to do this with SwiftUI?
Any help is appreciated.
Thanks in advance.
My code:
LogInView:
struct LogInView: View {
var body: some View {
VStack {
Text("Welcome to Mamoot!")
.font(.largeTitle)
.fontWeight(.heavy)
Text("We are glad to have you here.")
Text("Please log in with your Mastodon or Twitter account to continue.")
.multilineTextAlignment(.center)
.lineLimit(4)
.padding()
Spacer()
FloatingTextField(title: "Username", placeholder: "Username", width: 300, type: "Username")
FloatingTextField(title: "Password", placeholder: "Password", width: 300, type: "password")
.padding(.top, -50)
Spacer()
ZStack {
Button(action: { /* go to HomeView() */ }) {
Text("Log in")
.foregroundColor(Color.white)
.bold()
.shadow(color: .red, radius: 10)
}
.padding(.leading, 140)
.padding(.trailing, 140)
.padding(.top, 15)
.padding(.bottom, 15)
.background(Color.red)
.cornerRadius(10)
}
.padding(.bottom)
}
}
}
HomeView:
struct HomeView: View {
var body: some View {
Text("Home Page")
}
}
Update: I got some time to update the answer, and add a transition. Note that I changed Group by VStack, otherwise the transition does not work.
You can alter the duration in the withAnimationcall (button closure).
I also moved some modifiers in your button, so the whole thing is "tappable". Otherwise, only tapping on the text of the button would trigger the action.
You can use an ObservedObject, an EnvironmentObject or a Binding. Here's an example with ObservedObject:
import SwiftUI
class Model: ObservableObject {
#Published var loggedIn = false
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
if model.loggedIn {
HomeView().transition(.opacity)
} else {
LogInView(model: model).transition(.opacity)
}
}
}
}
struct HomeView: View {
var body: some View {
Text("Home Page")
}
}
struct LogInView: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text("Welcome to Mamoot!")
.font(.largeTitle)
.fontWeight(.heavy)
Text("We are glad to have you here.")
Text("Please log in with your Mastodon or Twitter account to continue.")
.multilineTextAlignment(.center)
.lineLimit(4)
.padding()
Spacer()
// FloatingTextField(title: "Username", placeholder: "Username", width: 300, type: "Username")
// FloatingTextField(title: "Password", placeholder: "Password", width: 300, type: "password")
// .padding(.top, -50)
Spacer()
ZStack {
Button(action: {
withAnimation(.easeInOut(duration: 1.0)) {
self.model.loggedIn = true
}
}) {
Text("Log in")
.foregroundColor(Color.white)
.bold()
.shadow(color: .red, radius: 10)
// moved modifiers here, so the whole button is tappable
.padding(.leading, 140)
.padding(.trailing, 140)
.padding(.top, 15)
.padding(.bottom, 15)
.background(Color.red)
.cornerRadius(10)
}
}
.padding(.bottom)
}
}
}
The answer by #kontiki is probably the most SwiftUI-y, but I will present a different solution, probably not as good! But maybe more flexible/scalable.
You can swap rootView of UIHostingController:
SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
fileprivate lazy var appCoordinator: AppCoordinator = {
let rootViewController: UIHostingController<AnyView> = .init(rootView: EmptyView().eraseToAny())
window?.rootViewController = rootViewController
let navigationHandler: (AnyScreen, TransitionAnimation) -> Void = { [unowned rootViewController, window] (newRootScreen: AnyScreen, transitionAnimation: TransitionAnimation) in
UIView.transition(
with: window!,
duration: 0.5,
options: transitionAnimation.asUIKitTransitionAnimation,
animations: { rootViewController.rootView = newRootScreen },
completion: nil
)
}
return AppCoordinator(
dependencies: (
securePersistence: KeyValueStore(KeychainSwift()),
preferences: .default
),
navigator: navigationHandler
)
}()
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
self.window = .fromScene(scene)
appCoordinator.start()
}
}
enum TransitionAnimation {
case flipFromLeft
case flipFromRight
}
private extension TransitionAnimation {
var asUIKitTransitionAnimation: UIView.AnimationOptions {
switch self {
case .flipFromLeft: return UIView.AnimationOptions.transitionFlipFromLeft
case .flipFromRight: return UIView.AnimationOptions.transitionFlipFromRight
}
}
}
AppCoordinator
And here is the AppCoordinator:
final class AppCoordinator {
private let preferences: Preferences
private let securePersistence: SecurePersistence
private let navigationHandler: (AnyScreen, TransitionAnimation) -> Void
init(
dependencies: (securePersistence: SecurePersistence, preferences: Preferences),
navigator navigationHandler: #escaping (AnyScreen, TransitionAnimation) -> Void
) {
self.preferences = dependencies.preferences
self.securePersistence = dependencies.securePersistence
self.navigationHandler = navigationHandler
}
}
// MARK: Internal
internal extension AppCoordinator {
func start() {
navigate(to: initialDestination)
}
}
// MARK: Destination
private extension AppCoordinator {
enum Destination {
case welcome, getStarted, main
}
func navigate(to destination: Destination, transitionAnimation: TransitionAnimation = .flipFromLeft) {
let screen = screenForDestination(destination)
navigationHandler(screen, transitionAnimation)
}
func screenForDestination(_ destination: Destination) -> AnyScreen {
switch destination {
case .welcome: return AnyScreen(welcome)
case .getStarted: return AnyScreen(getStarted)
case .main: return AnyScreen(main)
}
}
var initialDestination: Destination {
guard preferences.hasAgreedToTermsAndPolicy else {
return .welcome
}
guard securePersistence.isAccountSetup else {
return .getStarted
}
return .main
}
}
// MARK: - Screens
private extension AppCoordinator {
var welcome: some Screen {
WelcomeScreen()
.environmentObject(
WelcomeViewModel(
preferences: preferences,
termsHaveBeenAccepted: { [unowned self] in self.start() }
)
)
}
var getStarted: some Screen {
GetStartedScreen()
.environmentObject(
GetStartedViewModel(
preferences: preferences,
securePersistence: securePersistence,
walletCreated: { [unowned self] in self.navigate(to: .main) }
)
)
}
var main: some Screen {
return MainScreen().environmentObject(
MainViewModel(
preferences: preferences,
securePersistence: securePersistence,
walletDeleted: { [unowned self] in
self.navigate(to: .getStarted, transitionAnimation: .flipFromRight)
}
)
)
}
}