Swiftui: How to Present View When Clicking on a Button

Present a new view in SwiftUI

To show a modal (iOS 13 style)

You just need a simple sheet with the ability to dismiss itself:

struct ModalView: View {
@Binding var presentedAsModal: Bool
var body: some View {
Button("dismiss") { self.presentedAsModal = false }
}
}

And present it like:

struct ContentView: View {
@State var presentingModal = false

var body: some View {
Button("Present") { self.presentingModal = true }
.sheet(isPresented: $presentingModal) { ModalView(presentedAsModal: self.$presentingModal) }
}
}

Note that I passed the presentingModal to the modal so you can dismiss it from the modal itself, but you can get rid of it.



To make it REALLY present fullscreen (Not just visually)

You need to access to the ViewController. So you need some helper containers and environment stuff:

struct ViewControllerHolder {
weak var value: UIViewController?
}

struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController)

}
}

extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}

Then you should use implement this extension:

extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, @ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: "dismissModal"), object: nil, queue: nil) { [weak toPresent] _ in
toPresent?.dismiss(animated: true, completion: nil)
}
self.present(toPresent, animated: true, completion: nil)
}
}

Finally

you can make it fullscreen like:

struct ContentView: View {
@Environment(\.viewController) private var viewControllerHolder: UIViewController?

var body: some View {
Button("Login") {
self.viewControllerHolder?.present(style: .fullScreen) {
Text("Main") // Or any other view you like
// uncomment and add the below button for dismissing the modal
// Button("Cancel") {
// NotificationCenter.default.post(name: Notification.Name(rawValue: "dismissModal"), object: nil)
// }
}
}
}
}

Show a new View from Button press Swift UI

For simple example you can use something like below

import SwiftUI

struct ExampleFlag : View {
@State var flag = true
var body: some View {
ZStack {
if flag {
ExampleView().tapAction {
self.flag.toggle()
}
} else {
OtherExampleView().tapAction {
self.flag.toggle()
}
}
}
}
}
struct ExampleView: View {
var body: some View {
Text("some text")
}
}
struct OtherExampleView: View {
var body: some View {
Text("other text")
}
}

but if you want to present more view this way looks nasty

You can use stack to control view state without NavigationView

For Example:

    class NavigationStack: BindableObject {
let didChange = PassthroughSubject<Void, Never>()

var list: [AuthState] = []

public func push(state: AuthState) {
list.append(state)
didChange.send()
}
public func pop() {
list.removeLast()
didChange.send()
}
}

enum AuthState {
case mainScreenState
case userNameScreen
case logginScreen
case emailScreen
case passwordScreen
}
struct NavigationRoot : View {
@EnvironmentObject var state: NavigationStack
@State private var aligment = Alignment.leading

fileprivate func CurrentView() -> some View {
switch state.list.last {
case .mainScreenState:
return AnyView(GalleryState())
case .none:
return AnyView(LoginScreen().environmentObject(state))
default:
return AnyView(AuthenticationView().environmentObject(state))
}
}
var body: some View {
GeometryReader { geometry in
self.CurrentView()
.background(Image("background")
.animation(.fluidSpring())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height,
alignment: self.aligment))
.edgesIgnoringSafeArea(.all)
.onAppear {
withAnimation() {
switch self.state.list.last {
case .none:
self.aligment = Alignment.leading
case .passwordScreen:
self.aligment = Alignment.trailing
default:
self.aligment = Alignment.center
}
}
}
}
.background(Color.black)
}

}

struct ExampleOfAddingNewView: View {
@EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.push(state: .emailScreen) }){
Text("Tap me")
}

}
}
}


struct ExampleOfRemovingView: View {
@EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.pop() }){
Text("Tap me")
}
}
}
}

In my opinion this bad way, but navigation in SwiftUI much worse

SwiftUI: How to present view when clicking on a button?

Much improved version (SwiftUI, iOS 13 beta 7)

The same solution works for dismissing Modals presented with the .sheet modifier.

import SwiftUI

struct DetailView: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(
"Here is Detail View. Tap to go back.",
action: { self.presentationMode.wrappedValue.dismiss() }
)
}
}

struct RootView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView())
{ Text("I am Root. Tap for Detail View.") }
}
}
}

struct ContentView: View {
var body: some View {
NavigationView {
RootView()
}
}
}

SwiftUI - Opening a new view on button click

A NavigationLink must be in view hierarchy, so instead of putting it in action we need to put some model there.

A sketch of possible approach

  1. destination model
enum MenuDestination: String, CaseIterable, Hashable {
case set1(MenuItem), set2

@ViewBuilder var view: some View {
switch self {
case .set1(let item): View1(item: item)
case .set2: SettingsView()
}
}
}

  1. navigation link in view
    @State private var selection: MenuDestination?
var isActive: Binding<Bool> {
Binding(get: { selection != nil }, set: { selection = nil } )
}

var body: some View {
NavigationView {
ScrollView {
// ...
}
.background(
if let selection = selection {
NavigationLink(isActive: isActive, destination: { selection.view }) {
EmptyView()
}})
}
}

  1. button action assigns corresponding value, say MenuItemAction take as argument binding to selection and internally assign destination to that binding
MenuItemCircularGridView(imageName: item.imageName, menuItemName: item.name, 
action: (menuOptionsAction.menuActions.first {$0.id == item.id})?.action($selection) ?? { _ in })

and MenuItemAction inited with case of corresponding MenuDestination

See also this post

How to show a subview when clicking on a button in SwiftUI

You can use a ZStack to accomplish that.
I put together an example where the buttons trigger the appearance of the subview.

Main View:

struct MainView: View {

@State var isPressed = false

var body: some View {
ZStack(alignment: .bottom){
VStack{
Button {
isPressed = true
} label: {
Text("Button MainView")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)

if isPressed == true {
VStack{
SubView(isPressed: $isPressed)
}
.frame(maxWidth: .infinity, maxHeight: 100)
}

}
}
}

SubView:

struct SubView: View {

@State var isPressed: Binding<Bool>

var body: some View {
Button {
isPressed.wrappedValue = false
} label: {
Text("Button Subview")
}
}
}

You can replace or extend the button actions and frame sizes as you like.

If you want the button in the MainView to show and hide the SubView, you can use .toggle()

Button {
isPressed.toggle()
} label: {
Text("Button MainView")
}

Update View on button click SwiftUI

model is not declared as StateObject, so any change in it will not be reflected in the UI.

EDIT : there is a logic problem in your code :

  • model is not used as a var but as a link to static class properties
  • the theme is choosen via static properties from MemoryGameClass

As you use static properties of a class, the values are only used when something that use this properties is updated, not when the property is updated.

The theme should be a published var of EmojisMemoryGame and declared as a struct. So when something changes in properties of the theme, it will cause a redraw of the view.

Edit 2 : what I meant :
Theme as a struct :

struct MemoryGameTheme {
// List of themes
static let allThemes: [ChoiseTheme: MemoryGameTheme] =
[.car: MemoryGameTheme(choiseTheme: .car, themeEmojis: ["quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "✈️", "quot;, "quot;, "quot;, "quot;, "⛵️", "quot;], themeName: "Cars", colorCard: .red),
.animal: MemoryGameTheme(choiseTheme: .animal, themeEmojis: ["quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "‍❄️", "quot;, "quot;, "quot;], themeName: "Animals", colorCard: .gray),
.item: MemoryGameTheme(choiseTheme: .item, themeEmojis: ["quot;, "⌚️", "quot;, "⌨️", "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "☎️", "quot;, "⏰", "quot;, "quot;, "quot;, "quot;], themeName: "Items", colorCard: .purple),
.food: MemoryGameTheme(choiseTheme: .food, themeEmojis: ["quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;], themeName: "Food", colorCard: .yellow),
.face: MemoryGameTheme(choiseTheme: .face, themeEmojis: ["quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "☺️", "quot;, "quot;, "quot;], themeName: "Face", colorCard: .green)]

// Type of themes
enum ChoiseTheme: CaseIterable {
case car
case animal
case item
case food
case face
}
var choiseTheme: ChoiseTheme = .animal
var themeEmojis = ["quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "quot;, "‍❄️", "quot;, "quot;, "quot;]
var themeName = "Animals"
var colorCard = Color.gray

mutating func changeTheme(_ choiseTheme: ChoiseTheme) {
let randomTheme = ChoiseTheme.allCases.randomElement()!
// Initialise the theme from static dictionnary
let theme = Self.allThemes[randomTheme]!
self.choiseTheme = theme.choiseTheme
self.themeEmojis = theme.themeEmojis
self.themeName = theme.themeName
self.colorCard = theme.colorCard

print(randomTheme)
print(choiseTheme)
}

// Mutating func to change theme
mutating func refreshTheme() {
changeTheme(ChoiseTheme.car)
}
}

EmojiGame including theme as Published var :

class EmojiMemoryGame: ObservableObject {
// Create a new memory game based on specific theme
static func createMemoreGame(theme: MemoryGameTheme) -> MemoryGame<String> {
return MemoryGame<String>(numberOfPairOfCards: theme.themeEmojis.count) { pairIndex in
theme.themeEmojis[pairIndex]
}
}

// This model is a struct
@Published private var model: MemoryGame<String> = EmojiMemoryGame.createMemoreGame(theme: MemoryGameTheme())
// This is the theme of the model
@Published private var theme: MemoryGameTheme = MemoryGameTheme()

// To acces model and theme comtents
var themeName: String {
theme.themeName
}
var colorCard: Color {
theme.colorCard
}

var cards: Array<MemoryGame<String>.Card> {
model.cards
}

// MARK: - Intens(s)
func choose(_ card: MemoryGame<String>.Card) {
model.choose(card)
}

// When refreshing theme, create a new game with teh new theme
func refreshTheme() {
theme.refreshTheme()
model = Self.createMemoreGame(theme: theme)
}
}

The Content view using ObservedObject Emoji game :

struct ContentView: View {
// emoji memory game must declared as Observed object
// to enable SwiftUI to update when something change in it
@ObservedObject var emojisGame: EmojiMemoryGame
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 75))]) {
ForEach (emojisGame.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
emojisGame.choose(card)
}
}
}
}
// Use theme color for cards
.foregroundColor(emojisGame.colorCard)
Spacer()
HStack {
newGame
Spacer()
nameTheme
}
}
.padding(.horizontal)
}

var newGame: some View {
Button(action: {
// Reset emoji game with theme change
emojisGame.refreshTheme()
self.onActivate()
}, label: {
Text("New Game")
.font(.title)
.fontWeight(.heavy)
})
}
var nameTheme: some View {
Text ("\(emojisGame.themeName)")
.font(.title3)
.fontWeight(.heavy)
}

func onActivate() {

}

}

Navigating to detail view on button click in List Row(TableviewCell) in SwiftUI

  1. Use the .hidden() modifier on the navigationLink to prevent the list item from capturing it's action

  2. Change the .buttonStyle from automatic to something else on the list item to prevent it from capturing the button action. (for example borderless)

Working Demo

struct ContentView: View {
@State var selectedTag: Int?

var body: some View {
NavigationView {
List(1...10, id: \.self) { id in
HStack {
Text("Item \(id: id)")
Spacer()

Button("Show detail") { selectedTag = id }
.background(link(id))
}.buttonStyle(.borderless) /// ⚠️ on the item! **NOT** on the button!!!
}
}
}

func link(id: Int) -> some View {
NavigationLink("",
destination: Text("\(id) Selected"),
tag: id,
selection: $selectedTag
).hidden()
}
}

Show NSMenu when clicking on a Button in SwiftUI view

Following the suggestion of vanadian in the comment, the way to go is to use Menu to get the result. This is how I did:

HStack {
Spacer(minLength: 100)

Menu("quot;) {
Button("Menu.Preferences".localized) {
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
Divider()
Button("Menu.Quit".localized) {
NSApp.terminate(self)
}
}
.frame(width: 30)
.menuStyle(BorderlessButtonMenuStyle())
}

This is a SwiftUI solution to reach the result I wanted.



Related Topics



Leave a reply



Submit