I have a SwiftUI view that I want to display programmatically. I currently present the screen with a button but I want to present it programmatically
This is the view:
struct SearchResultView: View {
#ObservedObject var model: SearchResultViewModel
var body: some View {
VStack {
VStack{
Text("Check Aisles ")
HStack{
ForEach(0 ..< model.aisleArry.count){aisleNum in
Text(String(self.model.aisleArry[aisleNum])).bold()
}
}
Text( "For Blue Coffee")
}
Spacer()
VStack {
ForEach(0 ..< Global.productArry.count) { value in
Text(Global.productArry[value].name)
}
}
Spacer()
}.onAppear { self.model.getValue() }
}
}
This is the button that presents it:
struct homeMainView: View {
var body: some View {
Button("Search") {
// show the search sheet
self.searchSheet.toggle()
}
.sheet(isPresented: $searchSheet) {
ProductSearchView(model: self.searchModel)
//
}
VStack {
if self.speechRecognition.isPlaying {
VStack {
Text(self.speechRecognition.recognizedText).bold()
}.onAppear{
//if text is recognize wait a few seconds and launch searchResult View
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// wait 1 second
// I WANT TO PRESENT IT PROGRAMATICALLY HERE
ProductSearchView(model:self.searchModel).presentationMode.wrappedValue.isPresented
self.sheet(isPresented: self.$searchSheet) {
ProductSearchView(model: self.searchModel)
}
}
}
}
}
}
I have tried many methods but could not get it to work. How can I present the SearchResultView programmatically?
Here is a possible solution
VStack {
if self.speechRecognition.isPlaying {
VStack {
Text(self.speechRecognition.recognizedText).bold()
}.onAppear{
//if text is recognize wait a few seconds and launch searchResult View
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// wait 1 second
self.searchSheet.toggle() // << activate here !!
}
}
}
}
.sheet(isPresented: $searchSheet) { // << attach here !!
ProductSearchView(model: self.searchModel)
}
Related
I am trying to make a SwiftUI ScrollView scroll to a certain point in an abstracted view when a button is pressed in a view which is calling the abstracted view programmatically. Here is my code:
struct AbstractedView: View {
#Namespace var view2ID
var body: some View {
ScrollView {
VStack {
View1()
View2()
.id(view2ID)
View3()
}
}
}
func scrollToView2(_ proxy: ScrollViewProxy) {
proxy.scrollTo(view2ID, anchor: .topTrailing)
}
}
As you can see, when scrollToView2() is called (in a ScrollViewReader), the AbstractedView scrolls to view2ID. I am creating a number of AbstractedView's programmatically in a different View:
struct HigherView: View {
var numAbstractedViewsToMake: Int
var body: some View {
VStack {
HStack {
ForEach (0..<numAbstractedViewsToMake, id: \.self) { _ in
AbstractedView()
}
}
Text("button")
.onTapGesture {
/* call each AbstractedView.scrollToView2()
}
}
}
}
If I stored these views in an array in a struct inside my HigherView with a ScrollViewReader for each AbstractedView would that work? I feel as though there has to be a nicer way to achieve this, I just have no clue how to do it. I am new to Swift so thank you for any help.
P.S. I have heard about UIKit but I don't know anything about it, is this the right time to be using that?
Using the comments from #Asperi and #jnpdx, I was able to come up with a more powerful solution than I needed:
class ScrollToModel: ObservableObject {
enum Action {
case end
case top
}
#Published var direction: Action? = nil
}
struct HigherView: View {
#StateObject var vm = ScrollToModel()
var numAbstractedViewsToMake: Int
var body: some View {
VStack {
HStack {
Button(action: { vm.direction = .top }) { // < here
Image(systemName: "arrow.up.to.line")
.padding(.horizontal)
}
Button(action: { vm.direction = .end }) { // << here
Image(systemName: "arrow.down.to.line")
.padding(.horizontal)
}
}
Divider()
HStack {
ForEach(0..<numAbstractedViewsToMake, id: \.self) { _ in
ScrollToModelView(vm: vm)
}
}
}
}
}
struct AbstractedView: View {
#ObservedObject var vm: ScrollToModel
let items = (0..<200).map { $0 } // this is his demo
var body: some View {
VStack {
ScrollViewReader { sp in
ScrollView {
LazyVStack { // this bit can be changed accordingly
ForEach(items, id: \.self) { item in
VStack(alignment: .leading) {
Text("Item \(item)").id(item)
Divider()
}.frame(maxWidth: .infinity).padding(.horizontal)
}
}.onReceive(vm.$direction) { action in
guard !items.isEmpty else { return }
withAnimation {
switch action {
case .top:
sp.scrollTo(items.first!, anchor: .top)
case .end:
sp.scrollTo(items.last!, anchor: .bottom)
default:
return
}
}
}
}
}
}
}
}
Thank you both!
I am trying to show a pause button if a sound file is playing, i have a uniform source of truth for the sound file, which i can access via ViewModel, now all works well on other Views, but on parent View where all navigation links are, when i go back to it using the back button from other Views, the miniplayer that shows pause disappears...
So i decided that on the .onAppear of NavigationView or text view of parent View i will implement the logic that can detect if a sound file is playing and if so , show a button at bottom to pause the sound file.
Now i can use print and it shows correct value on onAppear in terms of sound file playing or not, but the moment i try to use HStack or any other View to be added i get warning -
Result of 'HStack<Content>' initializer is unused
Now if i decide to use State then also i get similar warning, how can i make the View rerender onAppear, or is that not possible, if that is the case from where i can implement this logic, thanks ....
import Foundation
import SwiftUI
struct HomePageTabView: View {
#Binding var songLVM: SongListVM
#State var miniBar: Bool = false
init(songLVM: Binding<SongListVM>){
self._songLVM = songLVM
UITableView.appearance().backgroundColor = UIColor(.white)
}
var body: some View {
NavigationView {
List {
//Artists
NavigationLink(
destination: ArtistList(songLVM: $songLVM))
{
HStack {
Image(systemName: "music.mic")
Text("Artists")
}
}
//Albums
NavigationLink(
destination: Text("Albums"))
{
HStack {
Image(systemName: "music.note.list")
Text("Albums")
}
}
//Collections
NavigationLink(
//destination: ArtistView())
destination: ArtistViewMain( songLVM: $songLVM))
{
HStack {
Image(systemName: "music.quarternote.3")
Text("Collections")
}
}
//About Us
NavigationLink(
destination: Text("About Us"))
{
HStack {
Image(systemName: "music.note.house.fill")
Text("About Us")
}
}
//Contact Us
NavigationLink(
destination: ArtistView())
{
HStack {
Image(systemName: "phone.circle")
Text("Contact Us")
}
}
}
}
.onAppear {
if(songLVM.audioPlayer?.isPlaying != nil){
HStack {
Button("Stop") {
songLVM.audioPlayer?.stop()
}
}
}
}
}
}
I had also tried
.onAppear{
miniBar.toggle()
if(miniBar == true){
HStack {
Text("Stop")
}
}
}
but got Result of 'HStack<Content>' initializer is unused
I will give easy and basic template for working with swift's ui states.
You can refer it and add your views or navigation link.
struct YourView: View {
/// If you want to pass it on init, use #ObservedObject instead
/// https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject
#StateObject var viewModel = YourViewModel()
var body: some View {
NavigationView {
VStack {
if viewModel.isPlaying {
Button {
viewModel.stop()
} label: {
Text("Stop")
}
} else {
Button {
viewModel.start()
} label: {
Text("Start")
}
}
Toggle(isOn: $viewModel.isPlaying) {
Text("isPlaying")
}
}
}
.onAppear {
viewModel.transform()
}
}
}
class YourViewModel: ObservableObject {
#Published var isPlaying = false
func transform() {
fetchStatus()
}
func fetchStatus() {
isPlaying = true
}
func stop() { isPlaying = false }
func start() { isPlaying = true }
}
I have the following example in SwiftUI:
import SwiftUI
struct DetailView: View {
var element:Int
#Binding var favList:[Int]
var body: some View {
Button(action: {
if !favList.contains(element){
favList.append(element)
}
else{
favList.removeAll(where: {$0 == element})
}
}){
HStack {
Image(systemName: (favList.contains(element)) ? "star.slash" : "star")
Text((favList.contains(element)) ? "Remove from favorites" : "Add to favorites")
}
.frame(maxWidth: 300)
}
}
}
struct MainView: View {
let elements = [1,2,3,4]
#State var favList:[Int] = []
var body: some View {
NavigationView {
List{
if !favList.isEmpty{
Section("Favorits"){
ForEach(elements, id: \.self){element in
if favList.contains(element){
NavigationLink(destination: DetailView(element: element, favList: $favList)) {
Text("\(element)")
}
}
}
}
}
Section("All elements"){
ForEach(elements, id: \.self){element in
if !favList.contains(element){
NavigationLink(destination: DetailView(element: element, favList: $favList)) {
Text("\(element)")
}
}
}
}
}
}
}
}
If I change the favList in the DetailView the view gets automatically dismissed. I guess this because the List structure changes.
Am I doing something wrong? Is this the intended behavior? How can I avoid this?
Best regards
i tried with this, it's the same code just fixing the section header. using the fav button in the view doesn't dismiss the view
import SwiftUI
struct DetailView: View {
var element:Int
#Binding var favList:[Int]
var body: some View {
Button(action: {
if !favList.contains(element){
favList.append(element)
}
else{
favList.removeAll(where: {$0 == element})
}
}){
HStack {
Image(systemName: (favList.contains(element)) ? "star.slash" : "star")
Text((favList.contains(element)) ? "Remove from favorites" : "Add to favorites")
}
.frame(maxWidth: 300)
}
}
}
struct MainView: View {
let elements = [1,2,3,4]
#State var favList:[Int] = []
var body: some View {
NavigationView {
List{
if !favList.isEmpty{
Section(header:Text("Favorits")){
ForEach(elements, id: \.self){element in
if favList.contains(element){
NavigationLink(destination: DetailView(element: element, favList: $favList)) {
Text("\(element)")
}
}
}
}
}
Section(header:Text("Favorits")){
ForEach(elements, id: \.self){element in
if !favList.contains(element){
NavigationLink(destination: DetailView(element: element, favList: $favList)) {
Text("\(element)")
}
}
}
}
}
}
}
}
Tried it on xcode 12,5 with iOS 14 as target
I am thinking of using a switch case to present different views.
struct searchview : View {
var body: some View {
VStack {
VStack {
if self.speechRecognition.isPlaying == true {
VStack {
Text(self.speechRecognition.recognizedText).bold()
.foregroundColor(.white)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.font(.system(size: 50))
.sheet(isPresented: $showSheet) {
self.sheetView
}
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.showSheet = true
}
}
}
}.onAppear(perform: getViews)
}
var currentSheetView: String {
var isProductDictEmpty = Global.productDict.isEmpty
var wasTextRecognizedEmpty = self.speechRecognition.recognizedText.isEmpty
var checkTextandDict = (wasTextRecognizedEmpty,isProductDictEmpty)
switch checkTextandDict {
case (true,true):
print("Product dict and Text rec are empty")
return "error"
case (false,false):
print("Yes we are in business")
return "product"
case (true,false):
print("OOPS we didnt catch that")
return "error"
case (false,true):
print("OOPS we didnt catch that")
return "zero match"
}
}
#ViewBuilder
var sheetView: some View {
if currentSheetView == "product" {
ProductSearchView(model: self.searchModel)
}
else if currentSheetView == "zero match" {
zeroResult()
}
else if currentSheetView == "error" {
SearchErrorView()
}
}
}
}
I know how to use .sheet modifier to present modal when a button is pressed but how could I the present the respective modals in swift cases automatically with button?
UPDATE
these are the views I am trying to implement
struct SearchErrorView: View {
var body: some View {
VStack {
Text("Error!")
Text("Oops we didn't catch that")
}
}
}
struct zeroResult: View {
var body: some View {
Text("Sorry we did not find any result for this item")
}
}
I'm not sure what I am doing wrongly. I tried to implement the solution below but still not able to call the views with the switch cases.
The solution is to programmatically set a variable controlling displaying of your sheet.
You can try the following to present your sheet in onAppear:
struct ContentView: View {
#State var showSheet = false
var body: some View {
Text("Main view")
.sheet(isPresented: $showSheet) {
self.sheetView
}
.onAppear {
self.showSheet = true
}
}
var currentSheetView: String {
"view1" // or any other...
}
#ViewBuilder
var sheetView: some View {
if currentSheetView == "view1" {
Text("View 1")
} else {
Text("View 2")
}
}
}
You may delay it as well:
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.showSheet = true
}
}
Just setting self.showSheet = true will present the sheet. It's up to you how you want to trigger it.
In the code below if I change the value of the TextField and then click "Cancel" (i.e. will not then do a coredata save), after this modal view is hidden the value is changed in the parents UI list?
Is this line effectively passing my ref? If yes how to change to be effectively by value?
UPDATE: Actually it appears the code in the Save button is getting call directly after the Code in the cancel button, that is in the case I'm clickon on Cancel. Not sure why this would be occurring?
Code:
import SwiftUI
struct GCListsViewEdit: View {
#Environment (\.presentationMode) var presentationMode
#State var titleStr : String = ""
var gcItem : GCList?
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter Details")) {
TextField("List Title", text: self.$titleStr)
.onAppear {
self.titleStr = self.gcItem?.title ?? "" // ** HERE **
}
}
HStack {
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save") {
guard !self.titleStr.isEmpty else {
return
}
guard let item = self.gcItem else {
return
}
item.title = self.titleStr
GCCoreData.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Edit List")
}
}
}
PARENT - just the body part
var body : some View {
NavigationView {
VStack {
// -- Main List --
List() {
ForEach(gcLists) { gcList in
HStack {
if self.editMode {
Button(action: {}) {
Text("\(gcList.title)")
}
.onTapGesture {
self.selectedListViewItem = gcList
self.newListItemTitle = gcList.title
self.showEditView.toggle()
}
.sheet(isPresented: self.$showEditView, content: {
GCListsViewEdit(gcItem: self.selectedListViewItem!)
})
} else {
NavigationLink(destination: GCTasksView(withGcList: gcList)) {
Text("\(gcList.title)")
}
}
}
}
.onDelete(perform: self.deleteList)
.onMove(perform: self.move)
}
.environment(\.editMode, editMode ? .constant(.active) : .constant(.inactive))
.alert(isPresented: $showingAlert) {
Alert(
title: Text(verbatim: "Important Message"),
message: Text(self.alertString),
dismissButton: Alert.Button.default(Text(verbatim: "Cancel"))
)
}
.navigationBarTitle( Text("Todo Lists") )
.navigationBarItems(
trailing: Button(action: {
print("Edit" as Any)
self.editMode = !self.editMode
} ) {
Text(editMode ? "Done" : "Edit")
}
)
// -- Add List Item ---selectedListViewItem
Button("Add List") {
self.newListItemTitle = ""
self.showAddView.toggle()
}
.sheet(isPresented: $showAddView, content: { GCListsViewAdd() } )
}
}
}
Form is kind of List and List has specific handling of standard buttons in a row - it makes active entire row, so when row tapped in any place a button (or buttons) got activated.
In your example even if you tap in between Cancel and Save, both action are executed.
There are several possible solutions:
1) use tap gestures (keeping buttons or replacing them with other views, Text, Image, etc.), like
HStack {
Button("Cancel") {}
.onTapGesture {
print(">> do cancel")
}
Spacer()
Button("Save") {}
.onTapGesture {
print(">> do save")
}
}
2) use custom button style, because List intercepts only DefaultButtonStyle buttons
struct CustomButton: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(configuration.isPressed ? Color.gray : Color.blue)
}
}
HStack {
Button("Cancel") {
print(">> do cancel")
}.buttonStyle(CustomButton())
Spacer()
Button("Save") {
print(">> do save")
}.buttonStyle(CustomButton())
}
3) move buttons out of Form for this screen (eg. in NavigationBar)