Reload SwiftUI View from a hosting controller - ios

Is there a way to reload a swiftUI view from its hosting controller?
I have a WKHostingController with a swiftUI view for its body. When notification outside of the view gets fired, I need to update the view. If I try to change a #Binding or #State variable, I'm getting error
I can't update values outside of the view
But I can't find a refresh method.
Any ideas?
Hosting Controller:
class WorkoutListInterfaceController : WKHostingController<WorkoutListView> {
override var body: WorkoutListView {
WorkoutListView(host: self)
}
override func didAppear() {
// update WorkoutListView!!!
}
func showExercises(forWorkout workout: Workout) {
WKInterfaceController.reloadRootControllers(withNamesAndContexts: [(name: "ExerciseListInterfaceController", context: workout as AnyObject)])
}
func showAddWorkout() {
presentController(withNamesAndContexts: [("AddWorkoutHostingController", context: "" as AnyObject)])
}
}
SwiftUI View:
struct WorkoutListView : View {
weak var host: WorkoutListInterfaceController?
#State private var shouldPresentAddWorkout = false
#State var workouts: [Workout] = Workout.all()
init(host: WorkoutListInterfaceController) {
self.host = host
}
var body: some View {
return List {
Section(header: Text("Workouts")
.font(.system(size: 18))
.fontWeight(.bold)
.frame(height: 18))
{
ForEach(workouts) { workout in
Button(action: {
self.host?.showExercises(forWorkout: workout)
}) {
Text(workout.title)
}
}
}
Button(action: {
self.host?.showAddWorkout()
}) {
Text("Add Workout")
.fontWeight(.medium)
.foregroundColor(.green)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
}
}
.sheet(isPresented: $shouldPresentAddWorkout, onDismiss: {}) {
AddWorkoutView(host: AddWorkoutHostingController())
}
.contextMenu {
Button(action: { self.host?.showAddWorkout() },
label: {
VStack {
Image(systemName: "plus")
Text("Add Workout")
}
})
}
}
}
WorkoutListInterfaceController's didAppear() gets called and I want to update WorkoutListView.
On onAppear(), swiftUI view modifier doesn't get called. I don't know if this is a bug or expected behavior, that's why I am defaulting to the didAppear() on the hosting controller to refresh the view.

Got this to work with using Combine and NotificationCenter by posting a notification when the presented view will dismiss:
NotificationCenter.default.post(name: .updateWorkoutsNotification, object: workouts)
And then subscribing to that notification in a WorkoutListView view model

Have you tried:
. setNeedsBodyUpdate()
https://developer.apple.com/documentation/swiftui/wkhostingcontroller/setneedsbodyupdate()

Related

Add NavigationLink to Custom designed button in Swift UI

I am trying to add NavigationLink inside NavigationView in which there is a custom designed button.
I want to navigate on that button tap but NavigationLink code gives compilation error. it requires localisedTitle which I don't want to display and also tried to give Custom button in place of label but not working. Any help would be appreciated
Here is my code! I am using Xcode 13.3.1
#State private var isShowingSignupView = false
var body: some View {
NavigationView {
VStack {
Spacer()
VStack(alignment: .center, spacing: 12) {
AppTextField(Constants.Email, text: $email)
AppTextField(Constants.Password, text: $password, isSecure: $isSecure, leftImage: "lock",rightImage: Image(systemName: "eye.fill"), rightSelectedImage: Image(systemName: "eye.slash.fill"))
AppButtonText(title: Constants.ForgotPassword) {
debugPrint("Forgot Password tapped")
}
.frame(maxWidth: .infinity, maxHeight: 20, alignment: .trailing)
AppButton(title: Constants.Login) {
}
}
Spacer()
NavigationLink($isShowingSignupView, destination: Signup) {
AppButtonText(title: Constants.SignUp) {
isShowingSignupView = true
}
}
}
.padding()
}
.navigationTitle("Login")
}
**ERROR:-
Cannot convert value of type 'Signup.Type' to expected argument type '() -> Destination'
Generic parameter 'Destination' could not be inferred**
**I have also tried after replacing this **
NavigationLink(destination: Signup()) {
AppButtonText(title: Constants.SignUp) {
isShowingSignupView = true
}
}
Which just removed error but does not navigate on new screen
Hey there!
I got the issue and sollution to it.
Actually above code was almost correct but the problem was in Custom Button view action which was not getting triggered due to simultaneousGesture added in it for functionality
struct AppButtonText: View {
var title: String
#State var action: () -> ()
#State private var isPressed = false
var body: some View {
Text(title)
.foregroundColor(isPressed ? Color.red.opacity(0.5) : Color.red)
.background(Color.clear)
.padding(.vertical, 0)
.font(.body)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
isPressed = true
})
.onEnded({ _ in
isPressed = false
action() `THIS LINE WAS NOT ADDED BEFORE`
})
)
}
}
And this below line was having no issue
NavigationLink($isShowingSignupView, destination: Signup) {
AppButtonText(title: Constants.SignUp) {
isShowingSignupView = true
}
}
THANKS ALL FOR YOUR VALUABLE REPLIES
You need to use NavigationLink(destination: { Signup() })
Also since you are using custom Button instead of NavigationLink it's better use isActive property:
NavigationLink("Signup", isActive: $isShowingSignupView) {
Signup()
}
AppButtonText(title: Constants.SignUp) {
isShowingSignupView = true
}
You can just do it like this. Note that you need to initialize when using a view struct, like Signup().
#State private var isShowingSignupView = false
struct Signup: View {
var body: some View {
Text("Your view contents here")
}
}
var signup: some View {
Text("Your view contents here")
}
var body: some View {
NavigationView {
VStack {
// Using a view
NavigationLink(isActive: $isShowingSignupView) {
signup
} label: {
AppButtonText(title: Constants.SignUp) {
isShowingSignupView = true
}
}
// Using a view struct
NavigationLink(isActive: $isShowingSignupView) {
Signup()
} label: {
AppButtonText(title: Constants.SignUp) {
isShowingSignupView = true
}
}
}
}
}

SwiftUI Transition not happening

I am new to SwiftUI and I am trying to use the .transition, but for some reason no transition happens.
You can see the code below:
View
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
if self.viewModel.model.show {
Text("Showing")
.padding()
} else {
Text("Not Showing")
.padding()
.transition(.asymmetric(insertion: .scale, removal: .opacity))
}
Button {
self.viewModel.show()
} label: {
Text("Tap to change")
}
}
}
ViewModel
class ViewModel: ObservableObject {
#Published private(set) var model = Model()
func show() {
self.model.toggleShow()
}
}
Model
struct Model {
var show: Bool = true
mutating func toggleShow() {
self.show.toggle()
}
}
When I tap the button the text changes but no transition occurs.
I feel like I am missing something trivial here.
Can anyone please assist?
You need an animation (to animate transition) and a container (which performs actual transition, because default implicit Group does not do that).
Here is fixed part of code (tested with Xcode 13.2 / iOS 15.2)
*Note:Preview > Debug > Slow Animation for better visibility
var body: some View {
VStack { // << this !!
if self.viewModel.model.show {
Text("Showing")
.padding()
} else {
Text("Not Showing")
.padding()
.transition(.asymmetric(insertion: .scale, removal: .opacity))
}
}
.animation(.default, value: self.viewModel.model.show) // << here !!
Button {
self.viewModel.show()
} label: {
Text("Tap to change")
}
}
Your code is fine (besides the fact that you need a VStack wrapping the text and the button), you only need to tell SwiftUI to use the transition by wrapping the command inside withAnimation().
Here's what you simply need to do in ContentView (look at the Button):
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
if self.viewModel.model.show {
Text("Showing")
.padding()
} else {
Text("Not Showing")
.padding()
.transition(.asymmetric(insertion: .scale, removal: .opacity))
}
Button {
withAnimation { // This is what you need to trigger the transition
self.viewModel.show()
}
} label: {
Text("Tap to change")
}
}
.animation(.easeIn, value: self.viewModel.show)
}

SwiftUI NavigationLink for iOS 14.5 not working

I had the following code in Xcode 12.4 that worked perfectly
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 0) {
HStack {
if (type == "Quiz") {
NavigationLink(destination: Quiz(id: quiz.id)) {
VStack(alignment: .leading) {
Text("Quiz")
.font(.headline)
.foregroundColor(.white)
.padding(.top, 8)
.padding(.leading)
}
.background(Color.green)
.cornerRadius(12)
.shadow(color: .green, radius: 3, x: 0.0, y: 0.0)
}
} else {
NavigationLink(destination: Survey(id: survey.id)) {
VStack(alignment: .leading) {
Text("Survey")
.font(.headline)
.foregroundColor(.white)
.padding(.top, 8)
.padding(.leading)
}
.background(Color.green)
.cornerRadius(12)
.shadow(color: .green, radius: 3, x: 0.0, y: 0.0)
}
} // End If
if (type == "Quiz") {
NavigationLink(destination: QuizResults(id: quiz.id)) {
VStack(alignment: .leading) {
Text("Quiz Results")
.font(.headline)
.foregroundColor(.white)
.padding(.top, 8)
.padding(.leading)
}
.background(Color.blue)
.cornerRadius(12)
.shadow(color: .blue, radius: 3, x: 0.0, y: 0.0)
}
} else {
NavigationLink(destination: SurveyResults(id: survey.id)) {
VStack(alignment: .leading) {
Text("Survey Results")
.font(.headline)
.foregroundColor(.white)
.padding(.top, 8)
.padding(.leading)
}
.background(Color.blue)
.cornerRadius(12)
.shadow(color: .blue, radius: 3, x: 0.0, y: 0.0)
}
}
}
.padding([.leading, .trailing], 25)
}
.frame(height: 100)
I just updated Xcode to 12.5 and the above does not work any more.
It was working fine in 12.4!?
Now when I click the 'Quiz' element, it starts the transition to the Quiz View which is displays it but immediately closes the view and I'm back in the Detail View!?
Can someone see what I am doing wrong, and why now based on the update to 12.5 this stopped working?
UPDATE
I refined the code to the minimal possible reproducible form. What seems to be happening is that I have two or more NavigationLinks sets.
the first is the set to navigate the user to either the Quiz or Survey which the if statement addresses the user to the correct view to fill in.
Where the issue is in 12.5 is that the second set where the user can click to go see the overall results of the Quiz or Survey does not work when it's directly after the first navigation.
Like I said before hand it worked perfectly in 12.4 but seems like 12.5 does not agree with it. Can someone offer a better way for the user to click an element to either go fill in a quiz or survey or go see the results of a quiz or survey?
I got exactly the same problem, everything works fine with Xcode 12.4.
https://developer.apple.com/forums/thread/677333
I try to following this thread, it might work but on some case, I still have this bug.
NavigationLink(destination: EmptyView()) {
EmptyView()
}
Apparently, you can put this 3 lines of code close to your NavigationLink...
If someone got a better answer I will really appreciate it !
What a horrible bug! From my testing and some googling it happens when there are exactly 2 navigation links in a view. The code in the question has 4 but because of the if else statements there are effectively only 2 at a time.
I often don't know how many nav links I will have as it depends on what data the user has added/how many search hits there are etc. To be safe I've made a tripleEmptyNavigationLink modifier which I've stuck at the end of all my views. It's solving the popping behaviour but I'm still getting the 'Unable to present' warnings. Would love to know if anyone has anything better than this!
import SwiftUI
struct TripleEmptyNavigationLink: View {
var body: some View {
VStack {
NavigationLink(destination: EmptyView()) {EmptyView()}
NavigationLink(destination: EmptyView()) {EmptyView()}
NavigationLink(destination: EmptyView()) {EmptyView()}
}
}
}
struct TripleEmptyNavigationLinkBackground: ViewModifier {
func body(content: Content) -> some View {
content
.background(TripleEmptyNavigationLink())
}
}
extension View {
func tripleEmptyNavigationLink()-> some View {
self.modifier(TripleEmptyNavigationLinkBackground())
}
}
usage:
MyView()
.tripleEmptyNavigationLink()
In Xcode13 beta still has this issue.
So far solution:
1、Wrap NavigationLink with List or Form:
List {
NavigationLink(destination: Text("1")) {
Text("1")
}
NavigationLink(destination: Text("2")) {
Text("2")
}
NavigationLink(destination: Text("3")) {
Text("3")
}
}
2、Or Use one NavigationLink, and create destination view from func:
struct TaskIndexPage: View {
func buildView() -> some View {
// return you destination
switch self.option {
case 1:
return Text("\(option)")
default:
return Text("\(option)")
}
}
#State private var showDetail: Bool = false
#State private var option: Int = 0
var body: some View {
VStack {
Button {
showDetail = true
option = 1
} label: { Text("button 1") }
Button {
showDetail = true
option = 2
} label: { Text("button 2") }
Button {
showDetail = true
option = 3
} label: { Text("button 3") }
}
// handle navigating
NavigationLink(destination: self.buildView(), isActive: $showDetail) {}.opacity(0)
}
}
Adding a NavigationLink with an empty view didn't work for me. I solved my issue removing all NavigationLinks from the ForEach and using a single one to control the navigation to the detail view, a tap gesture and 2 state variables to keep track on what is being tapped on.
The example broken code and fix can be found at Paul Hudson's site.
https://www.hackingwithswift.com/forums/swiftui/unable-to-present-please-file-a-bug/7901/8237
Below is the complete working version
import SwiftUI
struct NavigationViewOptions {
enum OptionType { case main, optional }
typealias Option = (id: UUID, value: String, type: Self.OptionType)
static var options: [Option] = [
(UUID(), "Option 1", .main),
(UUID(), "Option 2", .optional),
(UUID(), "Option 3", .main),
(UUID(), "Option 4", .main),
(UUID(), "Option 5", .optional),
]
static func buildView(for option: Option) -> some View {
switch option.type {
case .main:
return Text("Main Option selected\n\(option.value)").font(.title).fontWeight(.bold)
case .optional:
return Text("Optional Option selected\n\(option.value)").font(.title3).italic().fontWeight(.medium)
}
}
}
struct NavigationViewWorking: View {
// State variables to leep track of what option has been tapped on and when to navigate to new view
#State private var selectedOption: NavigationViewOptions.Option = (id:UUID(),"",.main)
#State private var showDetail: Bool = false
var body: some View {
NavigationView {
ScrollView{
VStack (alignment:.leading) {
Text("NAVIGATION FIX FOR:\nUnable to present. Please file a bug.")
.padding(.bottom, 40)
ForEach(NavigationViewOptions.options, id: \.id) { option in
Text(option.value)
.font(.title)
.padding(.vertical, 10)
.foregroundColor(.accentColor) // same color as navigationLink
// handle tap on option
.onTapGesture {
selectedOption = option
showDetail = true
}
}
Spacer()
NavigationLink("", destination: NavigationViewOptions.buildView(for: selectedOption), isActive: $showDetail)
.opacity(0)
}
.navigationTitle("Options")
}
// INITIAL DETAIL VIEW
Text("Select option from the left")
}
}
}
For me the correct answer didn't work.
It showed Unable to present-message and then required view was pushed and poped out back quickly.
While playing around I found a working solution. I keep NotificationLink's without label set as a plain List items.
NavigationView {
ZStack {
List {
NavigationLink(isActive: $isFirstViewPresented,
destination: firstView,
label: EmptyView.init)
NavigationLink(isActive: $isSecondViewPresented,
destination: secondView,
label: EmptyView.init)
}
.listStyle(.plain)
//...
Button("Show first view") { isFirstViewPresented.toggle() }
Button("Show second view") { isSecondViewPresented.toggle() }
}
}
Don't forget to wrap active-properties with #State.
It also has some benefits as for me (all the navigation links are placed at the top of the view-getter and I don't need to look for it through all the code.
I could never find a reliable solution to this horrible bug. So I decided to create a custom NavigationLink, https://gist.github.com/Arutyun2312/a0dab7eecaa84bde99c435fecae76274. This works way better than expected, because all swiftui related functions continue working as usual. Seems like the bug is specifically with NavigationLink.
struct NavigationLink: View {
fileprivate init<T: View>(body: T) {
self.body = .init(body)
}
let body: AnyView
}
private struct NavigationLinkImpl<Destination: View, Label: View>: View {
let destination: () -> Destination?
#State var isActive = false
#ViewBuilder let label: () -> Label
var body: some View {
NavigationLinkImpl1(destination: destination, isActive: $isActive, label: label)
}
}
private struct NavigationLinkImpl1<Destination: View, Label: View>: View {
let destination: () -> Destination
#Binding var isActive: Bool
#ViewBuilder let label: () -> Label
#State var model = Model()
var body: some View {
Button(action: action, label: label)
.introspectNavigationController(customize: handle)
.id(isActive)
}
func handle(nav: UINavigationController) {
if isActive {
if model.destination == nil {
let dest = UIHostingController<Destination>(rootView: destination())
nav.pushViewController(dest, animated: true)
model.destination = dest
}
} else {
if let dest = model.destination {
if let i = nav.viewControllers.lastIndex(of: dest) {
nav.setViewControllers(.init(nav.viewControllers.prefix(i + 1)), animated: true)
}
model.destination = nil
}
}
if isActive != model.contains(nav: nav) { // detect pop
isActive = model.contains(nav: nav)
}
}
final class Model {
var destination: UIHostingController<Destination>?
func contains(nav: UINavigationController) -> Bool { destination.map { nav.viewControllers.contains($0) } ?? false }
}
func action() { isActive = true }
}
extension NavigationLink {
init<Destination: View, Label: View>(destination: #autoclosure #escaping () -> Destination, #ViewBuilder label: #escaping () -> Label) {
self.init(body: NavigationLinkImpl(destination: destination, label: label))
}
init<Destination: View, Label: View>(destination: #autoclosure #escaping () -> Destination, isActive: Binding<Bool>, #ViewBuilder label: #escaping () -> Label) {
self.init(body: NavigationLinkImpl1(destination: destination, isActive: isActive, label: label))
}
init<Destination: View>(_ text: String, destination: #autoclosure #escaping () -> Destination, isActive: Binding<Bool>) {
self.init(destination: destination(), isActive: isActive) { Text(text) }
}
init<Destination: View>(_ text: String, destination: #autoclosure #escaping () -> Destination) {
self.init(destination: destination()) { Text(text) }
}
}
Put this in a file, and your existing NavigationLinks will work just fine. Tested in ios 14 and 15
Like anybody else on iOS 14.5.1 my application is hit by this awful bug. I have more than 3 NavigationLinks in the page, and I was not lucky to modify the numbers of the NavigationLinks (by adding a dummy NavigationLink) to get the correct behaviour.
A workaround that is Okay for me is to add a NavigationLink conditionally into the view.
Instead of:
var body: some View {
NavigationLink(destination: AnotherView(), isActive: $someCondition) { EmptyView() }
}
I have this:
var body: some View {
if someCondition {
NavigationLink(destination: AnotherView(), isActive: $someCondition) { EmptyView() }
}
}
The behaviour is not exactly the same, as you lose some navigation animation candy, but at least you have a working application again with relatively easy to understand fix.
You can also short-circuit it to 14.5 only, and normal behaviour elsewhere:
/// Assumes this gets fixed by Apple until 14.6 is out
var onIOS14_5: Bool {
let systemVersion = UIDevice.current.systemVersion
return systemVersion.starts(with: "14.5")
}
var body: some View {
if !onIOS14_5 || someCondition {
NavigationLink(destination: AnotherView(), isActive: $someCondition) { EmptyView() }
}
}
Perhaps this helps someone and lets all hope Apple will fix this embarrasing bug. Now I want my half day back.
In my case, the NavigationLink didn't work because of an .onTapGesture I added to dismiss the keyboard.
I got exactly the same problem.
my code:
class NavigationManager: ObservableObject {
static let shared: NavigationManager = {
return NavigationManager()
}()
#Published var showingMain: Bool
#Published var showingSub: Bool
#Published var content: AnyView
init() {
showingMain = false
showingSub = false
content = AnyView(EmptyView())
}
func forward<T:View>(content: #escaping () -> T ) {
showView()
self.content = AnyView(content())
}
private func showView() {
if !showingMain,!showingSub {
showingMain = true
} else if showingMain,!showingSub {
showingSub = true
} else if !showingMain,showingSub {
showingMain = true
}
}
}
struct NavigationLinkGroup: View {
#EnvironmentObject var navigationManager: NavigationManager
var body: some View {
Group {
NavigationLink(destination: navigationManager.content, isActive: $navigationManager.showingMain) {EmptyView()}
NavigationLink(destination: navigationManager.content, isActive: $navigationManager.showingSub) {EmptyView()}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLinkGroup()
}
}
}
https://github.com/Ftrybe/CustomBackButtonOfSwiftUIApp/tree/master/CustomBackButtonOfSwiftUI
It seems that if there's more than one NavigationLink in NavigationView, this bug will be filed.
Here's my solution.
import SwiftUI
enum MyLink {
case myView1
case myView2
}
struct MyView1: View {
var body: some View {
Text("MyView1")
}
}
struct MyView2: View {
var body: some View {
Text("MyView2")
}
}
struct ExampleView: View {
#State var currentLink: MyLink = .myView1
#State var isLinkViewShow: Bool = false
func getLinkView(_ myLink: MyLink) -> some View {
if myLink == .myView1 {
return AnyView(MyView1())
} else {
return AnyView(MyView2())
}
}
var body: some View {
NavigationView {
VStack {
NavigationLink("",
destination: getLinkView(currentLink),
isActive: $isLinkViewShow)
// Press to navigate to MyView1
Button(action: {
currentLink = .myView1
isLinkViewShow = true
}) {
Text("To MyView1")
}
// Press to navigate to MyView2
Button(action: {
currentLink = .myView2
isLinkViewShow = true
}) {
Text("To MyView2")
}
}
}
}
}
Adding a delay gets auto-navigation working again.
NavigationLink(destination: PopupView(),isActive: $showView){}
&
.onAppear {
if (test()){
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {showView = true}
}
}

Present a new view in SwiftUI

I want to click a button and then present a new view like present modally in UIKit
I have already seen "How to present a new view using sheets", but I don't want to attach it to the main view as a modal sheet.
And I don't want to use NavigationLink, because I don't want a new view and old view have a navigation relationship.
Thanks for your help...
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)
// }
}
}
}
}
For iOS 14 and Xcode 12:
struct ContentView: View {
#State private var isPresented = false
var body: some View {
Button("Show Modal with full screen") {
self.isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}
}
struct FullScreenModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("This is a modal view")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
See also: How to present a full screen modal view using fullScreenCover()
Disclaimer: Below is not really like a "native modal", neither behave nor look&feel, but if anyone would need a custom transition of one view over other, making active only top one, the following approach might be helpful.
So, if you expect something like the following
Here is a simple code for demo the approach (of corse animation & transition parameters can be changed by wish)
struct ModalView : View {
#Binding var activeModal: Bool
var body : some View {
VStack {
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.activeModal = false
}
}) {
Text("Hide modal")
}
Text("Modal View")
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.green)
}
}
struct MainView : View {
#Binding var activeModal: Bool
var body : some View {
VStack {
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.activeModal = true
}
}) {
Text("Show modal")
}
Text("Main View")
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.yellow)
}
}
struct ModalContainer: View {
#State var showingModal = false
var body: some View {
ZStack {
MainView(activeModal: $showingModal)
.allowsHitTesting(!showingModal)
.disabled(showingModal)
if showingModal {
ModalView(activeModal: $showingModal)
.transition(.move(edge: .bottom))
.zIndex(1)
}
}
}
}
Here is a simple one way - forward views. It's very straight forward.
struct ChildView: View{
private let colors: [Color] = [.red, .yellow,.green,.white]
#Binding var index : Int
var body: some View {
let next = (self.index+1) % MyContainer.totalChildren
return ZStack{
colors[self.index % colors.count]
Button("myNextView \(next) ", action: {
withAnimation{
self.index = next
}
}
)}.transition(.asymmetric(insertion: .move(edge: .trailing) , removal: .move(edge: .leading) ))
}
}
struct MyContainer: View {
static var totalChildren = 10
#State private var value: Int = 0
var body: some View {
HStack{
ForEach(0..<(Self.totalChildren) ) { index in
Group{
if index == self.value {
ChildView(index: self.$value)
}}
}
}
}
}
then presents it from ContentView when a button is tapped:
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
Button("Press to dismiss") {
dismiss()
}
.font(.title)
.padding()
.background(Color.black)
}
}
struct ContentView: View {
#State private var showingSheet = false
var body: some View {
Button("Show Sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
}
}

Change text based on toggle values in SwiftUI

I have created a View with Text, Image and Navigation Button in SwiftUI.When navigation button is pressed it will navigate to another view which contains Toggle.When I change the Toggle Value , I also want to change the Text value in the previous View.
Value is getting updated when changing the toggle but didn't reflected when accessed in previous View.
//BluetoothView.swift
struct BluetoothView: View {
#ObjectBinding var bluetooth = Settings()
var body: some View {
return NavigationButton(destination: ToggleBluetoothView()) {
HStack() {
Image("default")
.resizable()
.cornerRadius(12)
.frame(width: 25, height: 25)
.clipped()
.aspectRatio(contentMode: .fit)
Text("Bluetooth")
.color(.blue)
.font(.system(size: 18))
Text(bluetooth.isBluetoothOn ? "On" : "Off")
.color(.gray)
.font(.subheadline)
.frame(width: 50, height: 40, alignment: .trailing)
}
}
}
}
//ToggleBluetoothView.swift
struct ToggleBluetoothView: View {
#ObjectBinding var bluetooth = Settings()
var body: some View {
Form {
Section(header: Text("ENABLE TO CONNECT WITH NEARBY DEVICES")) {
Toggle(isOn: $bluetooth.isBluetoothOn) {
Text("Bluetooth")
}
}
}
}
}
//Settings.swift
class Settings: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var isBluetoothOn = false { didSet { update() } }
func update() {
didChange.send(())
}
}
You are instantiating Settings separately in each view. Both views need to be seeing the same Settings object:
Change the following:
NavigationButton(destination: ToggleBluetoothView(bluetooth: bluetooth)) { ... }
and remove the initial value in ToggleBluetoothView:
struct ToggleBluetoothView: View {
#ObjectBinding var bluetooth: Settings
var body: some View {
Form {
Section(header: Text("ENABLE TO CONNECT WITH NEARBY DEVICES")) {
Toggle(isOn: $bluetooth.isBluetoothOn) {
Text("Bluetooth")
}
}
}
}
}

Resources