I have the following code that, when tapped it will add the number of likes to a post, it will only allow the user to tap it once, works well but when I reload the app I can like it again, been trying to workout the best way to save that it has been tapped already. I have added the button state as false:
#State var buttonTapped = false
Button(action:
{
self.buttonTapped.toggle() //only allow one tap
let like = Int.init(post.likes)!
ref.collection("Posts").document(post.id).updateData(["likes": "\(like
+ 1)"]) { (err) in
if err != nil{
print((err!.localizedDescription))
return
}
// postData.getAllPosts()
print("updated...")
}
}
) {
Image(systemName: "flame")
.resizable()
.frame(width: 20, height: 20)
}.disabled(buttonTapped)
Any pointers in the right direction would be greatly appreciated
You can use UserDefaults to store the value
struct ContentView : View {
#State var buttonTapped : Bool = UserDefaults.standard.bool(forKey: "buttonTapped")
var body : some View {
Button(action: {
UserDefaults.standard.set(true, forKey: "buttonTapped")
buttonTapped.toggle()
}) {
Image(systemName: "flame")
.resizable()
.frame(width: 20, height: 20)
}.disabled(buttonTapped)
}
}
Related
Im displaying data from server and need to display the options in radiobuttons. But default none of the button should be selected. I was able to display radiobuttons. But when a particular button is clicked, only its image should be changed. Though with my code all the other button images too change. I have gone through some references SwiftUI - How to change the button's image on click?, but couldn't get it work. As am a newbie to SwiftUI, strucked here.
struct ListView: View {
#State var imageName: String = "radio-off"
var body: some View {
List(vwModel.OpnChoice.ItemList) { opn in
VStack(alignment:.leading) {
ForEach(opn.Choices.indices, id: \.self) { row in
Button(action: {
print("\(opn.Choices[row].ChoiceId)")
self.imageName = "radio-On"
}) {
Image(imageName)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
}
Text(opn.Choices[row].ChoiceName)
}
}
}
}
}
As you are working with indices anyway, add a #Published property to your view model which contains the selected index.
Then set the index in the button action. The redraw of the view sets the button at the selected index to the on-state and the others to the off-state.
As I don't know your environment this is a simplified stand-alone view model and view with SF Symbols images
class VMModel : ObservableObject {
#Published var selectedOption = -1
var numberOfOptions = 5
}
struct ListView: View {
#StateObject private var vwModel = VMModel()
var body: some View {
VStack(alignment:.leading) {
ForEach(0..<vwModel.numberOfOptions, id: \.self) { opn in
Button(action: {
vwModel.selectedOption = opn
print("index \(opn) selected")
}) {
Image(systemName: opn == vwModel.selectedOption ? "largecircle.fill.circle" : "circle")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
}
}
}
}
}
Update:
Meanwhile you can use a Picker with the .radioGroup modifier
enum Choice {
case one, two, three
}
struct ListView: View {
#State private var choice : Choice = .one
var body: some View {
Picker(selection: $choice, label: Text("Select an option:")) {
Text("One").tag(Choice.one)
Text("Two").tag(Choice.two)
Text("Three").tag(Choice.three)
}.pickerStyle(.radioGroup)
}
}
At the moment you are storing a single #State property "imageName" for the entire list. And then when any of the buttons in the list are tapped you are changing that single property. Which will affect all the buttons.
I'd suggest removing this property and putting something into the viewModel.
You appear to have a view model with an array called vwModel.opnChoice.itemList.
There are multiple ways of making this work. You could stop a boolean in the itemList to say whether each item is selected or not. But, as you want it to work like radio buttons what might be better is to have a property on the vwModel like selectedItem.
The button could then do...
vwModel.selectedItemId = opn.choices[row].choiceId
Then you button Label would be...
Image(vsModel.selectedItemId == opn.choices[row].choiceId ? "radio-on" : "radio-off")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
This would allow you to toggle the button when it is tapped and should turn the other buttons off.
I'm still new to SwiftUI and have run into a problem. I need to use back and forward buttons to make the Video Player go to the previous/next video (stored locally). The following code works for one video only, the one declared into the init(), but I can't manage to change the video by clicking the back and forward buttons.
I'm using an array of String called videoNames to pass all the video names from the previous View.
Also, I'm using a custom Video Player for this and I'm gonna include the relevant parts of the code.
This is my View:
struct WorkingOutSessionView: View {
let videoNames: [String]
#State var customPlayer : AVPlayer
#State var isplaying = false
#State var showcontrols = false
init(videoNames: [String]) {
self.videoNames = videoNames
self._customPlayer = State(initialValue: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: videoNames[0], ofType: "mov")!)))
}
var body: some View {
VStack {
CustomVideoPlayer(player: $customPlayer)
.frame(width: 390, height: 219)
.onTapGesture {
self.showcontrols = true
}
GeometryReader {_ in
// BUTTONS
HStack {
// BACK BUTTON
Button(action: {
// code
}, label: {
Image(systemName: "lessthan")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor((Color(red: 243/255, green: 189/255, blue: 126/255)))
.padding()
})
// FORWARD BUTTON
Button(action: {
// code
}, label: {
Image(systemName: "greaterthan")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor((Color(red: 243/255, green: 189/255, blue: 126/255)))
.padding()
})
}
}
}
.offset(y: 35)
.edgesIgnoringSafeArea(.all)
}
This is my custom Video Player:
struct CustomVideoPlayer : UIViewControllerRepresentable {
#Binding var player: AVPlayer
func makeUIViewController(context: UIViewControllerRepresentableContext<CustomVideoPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<CustomVideoPlayer>) {
}
}
I've researched for solutions but couldn't find anything relevant. I've tried to modify my CustomVideoPlayer and not pass a #Binding variable... As well, the init() gave me a lot of headaches as it comes back to errors every time I change something...
Any solution would help guys. I really appreciate your time.
The first thing that you'll need is something to keep track of your position in the video list. I'm using another #State variable for this.
Whenever that state variable changes, you'll need to update your player. I'm using the onChange modifier near the bottom of the code to do this work.
In your CustomVideoPlayer, you need to use updateUIViewController to make sure the player is up-to-date with the parameter being passed in.
Lastly, there's no need for AVPlayer to be a #Binding, since it's a class that is passed by reference, not a struct that is passed by value.
struct WorkingOutSessionView: View {
let videoNames: [String]
#State private var customPlayer : AVPlayer
#State private var currentItem = 0
#State var isplaying = false
#State var showcontrols = false
init(videoNames: [String]) {
self.videoNames = videoNames
self._customPlayer = State(initialValue: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: videoNames[0], ofType: "mov")!)))
}
var body: some View {
VStack {
CustomVideoPlayer(player: customPlayer)
.frame(width: 390, height: 219)
.onTapGesture {
self.showcontrols = true
}
.onAppear {
self.customPlayer.play()
}
GeometryReader { _ in
// BUTTONS
HStack {
// BACK BUTTON
Button(action: {
currentItem = min(currentItem, currentItem - 1)
}) {
Image(systemName: "lessthan")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor((Color(red: 243/255, green: 189/255, blue: 126/255)))
.padding()
}
// FORWARD BUTTON
Button(action: {
currentItem = min(videoNames.count - 1, currentItem + 1)
}) {
Image(systemName: "greaterthan")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor((Color(red: 243/255, green: 189/255, blue: 126/255)))
.padding()
}
}
}
}
.offset(y: 35)
.edgesIgnoringSafeArea(.all)
.onChange(of: currentItem) { currentItem in
print("Going to:",currentItem)
self.customPlayer.pause()
self.customPlayer = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: videoNames[currentItem], ofType: "mov")!))
self.customPlayer.play()
}
}
}
struct CustomVideoPlayer : UIViewControllerRepresentable {
var player: AVPlayer
func makeUIViewController(context: UIViewControllerRepresentableContext<CustomVideoPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<CustomVideoPlayer>) {
uiViewController.player = player
}
}
If this were my own project, I'd probably continue to do some refactoring -- maybe move some things to a view model, etc. Also, I'd probably avoid initializing an AVPlayer in a View's init as I mentioned in my last answer to you on your previous question. It works, but there's definitely a risk you'll end up doing too much heavy lifting if the view hierarchy re-renders.
I am presenting a "wizard" that will be detecting a BLE device and then if it is the correct one the last view will ask if we want to register or skip.
Edit:{
the view order is: MainView presenting in fullScreenCover a first info view informing on how to detect the BLE device then this one pushes a second view with some info on the nearest BLE device and it is in this view that we have the fork where I am presenting a sheet to ask if the user wants to continue and register the BLE device or skip.
So MAIN > INFOView -> BLE detection (> Register or skip ? RegisterView : Destack to main)
}
I have that last view come up as a sheet it has 2 buttons, the first one as mentioned says "Register" and the other one says "skip". If the user presses the register then we dismiss the sheet and navigate to a view that is gathering personal info to register the BLE device. on the other hand, if the user chooses to skip then the wizard need to de-stack back over to the main view.
Normally in UIKit I would just have a delegate inform me of the choice then if skip was selected. I would call pop to root view controller, otherwise, if the register option was selected I would dismiss the sheet view and then navigate to one more final view and get the user registered.
In SwiftUI I do not know how to deal with that navigation fork. I tried using PassthroughSubject but then I have to set the PassthroughSubject var as a state var and in the end, I just did not get the call back from sending in the selection.
Tried binding then Was hoping to make an onReceive but then it is asking for a publisher and that felt wrong to create a publisher just for that.
I am wondering g what is the best way do take care of this in. swiftUI ?
edit:
this is the code (updated with the replay from #Predrag Samardzic) for the view that shows the info on the BLE device (smart bike) and will push at first a request to know if the user wants to register or not, then if yes push that registration screen if not dismiss the entire stack.
struct A18BikeDiscoveryView: View {
#EnvironmentObject var bleManager: ArgonBLEManager
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack{
NavigationLink(
destination: A18RegistrationQuestionairy(QuestionairyViewModel()),
isActive: $isRegistrationPresented
) {
EmptyView()
}
A18ImageTextBanner(text: NSLocalizedString("bike_discovery_view_title", comment: ""))
.padding(.bottom, 35)
.navigationBarBackButtonHidden(true)
if let value = bleManager.model?.bikeInfo?.bikeModel{
Text(value)
.fontWeight(.bold)
.scaledFont(.largeTitle)
}
Image("subitoBike")
.resizable()
.frame(minWidth: 0334, idealWidth: 334, maxWidth: .infinity, minHeight: 223, idealHeight: 223, maxHeight: .infinity, alignment: .center)
.aspectRatio(contentMode: .fit)
.padding(.bottom, 10)
Divider()
VStack(alignment: .leading){
HStack{
Text("bike_discovery_view_year_created")
if let v = bleManager.model?.bikeInfo?.year{
Text(v)
}
}
HStack{
Text("bike_discovery_view_model_size")
Text("\(getSizeFromSerial())")
}
HStack{
Text("bike_discovery_view_bike_serial_number")
if let v = bleManager.model?.bikeInfo?.bikeSerialNumber {
Text(v)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 66, alignment: .leading)
.padding(.horizontal, 40)
Divider()
.padding(.bottom, 30)
Button(action: {
isSheetPresented = true
}, label: {
Text("bike_discovery_view_bike_pairing_button_title")
.fontWeight(.bold)
.foregroundColor(.white)
})
.buttonStyle(A18RoundButtonStyle(bgColor: .red))
.padding(.horizontal)
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
A18BikeParingSelection(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
Button(action: {
bleManager.disconect()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("bike_discovery_view_bike_pairing_cancel_button_title")
.fontWeight(.bold)
.foregroundColor(Color("grey55"))
})
.padding()
Spacer()
}
.navigationBarColor(backgroundColor: .white, tintColor: .black)
.navigationBarTitleDisplayMode(.inline)
}
func getSizeFromSerial() -> String {
if let serial = bleManager.model?.bikeInfo?.bikeSerialNumber {
if serial.contains("XXS"){
return "XXS"
}else if serial.contains("XSM") {
return "XS"
}else if serial.contains("SML"){
return "S"
}else if serial.contains("MED"){
return "M"
}else if serial.contains("LAR"){
return "L"
}
}
return "N/A"
}
}
This is one possible solution - using CurrentValueSubject in order to trigger dismiss and keep info about the choice made on the presented screen. Then, if registration is needed, you trigger it when sheet is dismissed.
struct MainView: View {
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack {
// this part is if you want to push registration screen, you will need to have MainView inside NavigationView for it
NavigationLink(
destination: RegistrationView(),
isActive: $isRegistrationPresented
) {
EmptyView()
}
// ----------------------------------------------------
Button {
isSheetPresented = true
} label: {
Text("Present sheet")
}
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
ChoiceView(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
// this part is if you want to present registration screen as sheet
// .sheet(
// isPresented: $isRegistrationPresented,
// content: {
// RegistrationView()
// })
}
}
}
struct ChoiceView: View {
let shouldShowRegistration: CurrentValueSubject<Bool, Never>
var body: some View {
VStack{
Button {
shouldShowRegistration.send(false)
} label: {
Text("Dismiss")
}
Button {
shouldShowRegistration.send(true)
} label: {
Text("Register")
}
}
}
}
struct RegistrationView: View {
var body: some View {
Text("Registration")
}
}
I tried to do a app that pop out a temporary alert that only appear for 1 or 2 seconds. It’s something like App Store rating.
But I don’t know what this called in swiftui. Can anyone answer me?
That is just a view that is shown or hidden conditionally. Here is a complete example that uses a ZStack to place the thank you view over the other view content. The thank you view is either present or not based upon the #State variable showThankYou. DispatchQueue.main.asyncAfter is used to remove the view after 3 seconds.
struct ContentView: View {
#State private var showThankYou = false
var body: some View {
ZStack {
VStack {
Spacer()
Text("Stuff in the view")
Spacer()
Button("submit") {
showThankYou = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.showThankYou = false
}
}
Spacer()
Text("More stuff in the View")
Spacer()
}
if showThankYou {
RoundedRectangle(cornerRadius: 16)
.foregroundColor(Color.gray)
.frame(width: 250, height: 250)
.overlay(
VStack {
Text("Submitted").font(.largeTitle)
Text("Thanks for your feedback").font(.body)
}
)
}
}
}
}
I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}