How to create slot-machine metaphor in SwiftUI Picker? - swiftui

Is it possible to create a slot machine with swiftUI to show two sets of values?

In UIKit, UIPickerView provides the option to have multiple components in your picker view. SwiftUI's Picker does not. However, you can use more than one Picker in an HStack instead. The perspective may look slightly different than a UIPickerView with multiple components in some instances, but to me it looks perfectly acceptable.
Here's an example of a slot machine with 4 pickers side by side and a button that "spins" the slot-machine when tapped (note that I disabled user interaction on the pickers so they can only be spun using the button):
enum Suit: String {
case heart, club, spade, diamond
var displayImage: Image {
return Image(systemName: "suit.\(self.rawValue).fill")
}
}
struct ContentView: View {
#State private var suits: [Suit] = [.heart, .club, .spade, .diamond]
#State private var selectedSuits: [Suit] = [.heart, .heart, .heart, .heart]
var body: some View {
VStack {
HStack(spacing: 0) {
ForEach(0..<self.selectedSuits.count, id: \.self) { index in
Picker("Suits", selection: self.$selectedSuits[index]) {
ForEach(self.suits, id: \.self) { suit in
suit.displayImage
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.clipped()
.disabled(true)
}
}
Button(action: self.spin) {
Text("Spin")
}
}
}
private func spin() {
self.selectedSuits = self.selectedSuits.map { _ in
self.suits.randomElement()!
}
}
}
This is just an example, and could no doubt be improved, but it's a decent starting point.
Keep in mind that this code does throw a warning in Xcode Beta 5 -
'subscript(_:)' is deprecated: See Release Notes for migration path.
I haven't had a chance to look into this, but the example still works and should help you with what you're trying to achieve.

Related

GeometryReader acting weird when presenting a modal on iOS 16. Bug or new behavior?

I'm seeing a weird behavior that is affecting one of my views in SwiftUI after upgrading to iOS 16.
Just to give some context, here is the stack:
Xcode 14
Simulator or real device on iOS 15.5 and 16
Considering the minimum reproducible code below:
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
GeometryReader { reader in
VStack(spacing: 36) {
Text("Screen frame:\n\(String(describing: reader.frame(in: .global)))")
.multilineTextAlignment(.center)
Button {
isPresented.toggle()
} label: {
Text("Open modal")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.onReceive(NotificationCenter.default.appDidBecomeActive()) { _ in
print(reader.frame(in: .global))
}
.onReceive(NotificationCenter.default.appDidEnterBackground()) { _ in
print(reader.frame(in: .global))
}
}
.sheet(isPresented: $isPresented) {
modalView
}
}
private var modalView: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}
extension NotificationCenter {
func appDidBecomeActive() -> AnyPublisher<Notification, Never> {
publisher(for: UIApplication.didBecomeActiveNotification).eraseToAnyPublisher()
}
func appDidEnterBackground() -> AnyPublisher<Notification, Never> {
publisher(for: UIApplication.didEnterBackgroundNotification).eraseToAnyPublisher()
}
}
As soon the view starts, it's possible to see the frame available due to the GeometryReader. Then following the steps:
Open the modal view
Send the app to the background
Open the app again
Close the modal
It's possible to see that the frame changed, and the values match with the 3D effect when a view is presenting another view, and it's never changing again to the right values unless you send the app again to the background or switch views (e.g. using a TabView).
I don't find anything on iOS release notes talking about it, so I supposed it must be a bug (I've filled out a bug report already).
On iOS 15, the frame value keeps stable at the same value.
I have a couple of views relying on the value of a GeometryReader, and it's causing my view to deform because of this issue. Does anyone know a way to force the recalculation for the GeometryReader for this case?
Any help is appreciated.
The issue won't occur if you control the display of the sheet with the new presentationDetents method, provided you do not request to cover the entire screen.
I modified your code as follows:
.sheet(isPresented: $isPresented) {
if #available(iOS 16, *) {
modalView
.presentationDetents([.fraction(0.99)])
}
else {
modalView
}
}
The issue will remain if you request .fraction(1), i.e. covering the whole screen.

Vertical alignment of HStack not working and not intuitive

I have this code:
HStack (alignment:.center) {
Text("50")
ProgressView("", value: 50, total: 100)
Text("100")
Text("mg")
}
the result is
I want this
I have tried everything like adding alignment center to HStack, adding the progress view inside a VStack, etc
Why Apple never manage to make things related to interface design work intuitively?
There is another way like not using "", and it works as you want.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Text("50")
ProgressView(value: 50, total: 100) // <<: Here
Text("100")
Text("mg")
}
}
}
The Version that you used is also good, but has another usage like this down code. And it needs the ProgressView pushed down for Text, actually, in your code ProgressView thinks that "" is the information to show! that is why you see that.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Text("50")
ProgressView("This part is for information to help", value: 50, total: 100)
Text("100")
Text("mg")
}
}
}

LazyVGrid onTapGesture navigate to next screen swiftUI

I am quite new to swiftUI. I have created a grid view on tapping on which I want to go to next screen. But somehow I am not able to manage to push to next screen. I am doing like this:
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: gridItems, spacing: 16) {
ForEach(viewModel.pokemon) { pokemon in
PokemonCell(pokemon: pokemon, viewModel: viewModel)
.onTapGesture {
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)) {
Text(pokemon.name)
}
}
}
}
}
.navigationTitle("Pokedex")
}
}
Upon doing like this, I am getting a warning stating
Result of 'NavigationLink<Label, Destination>' initializer is unused
Can someone please guide me, how to do this?
.onTapGesture adds an action to perform when the view recognizes a tap gesture. In your case you don't need to use .onTapGesture. If you want to go to another view when cell is tapped you need to write NavigationLink as below.
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)) {
PokemonCell(pokemon: pokemon, viewModel: viewModel)
}
If you want to use .onTapGesture, another approach is creating #State for your tapped cell's pokemon and using NavigationLink's isActive binding. So when user tap the cell it will change the #State and toggle the isActive in .onTapGesture. You may need to add another Stack (ZStack etc.) for this.
NavigationView {
ZStack {
NavigationLink("", destination: PokemonDetailView(pokemon: pokemon), isActive: $isNavigationActive).hidden()
ScrollView {
// ...

swiftui 2.0 Image Gallery onTapgesture only shows the first image in the array

I am creating a reusable gallery view for an app and am having difficulties when any picture is tapped it suppose to become full screen but only the first picture in the array is shown every time no matter the picture tapped. Below is my code, thanks.
import SwiftUI
struct ReusableGalleryView: View {
let greenappData: GreenAppNews
let gridLayout: [GridItem] = Array(repeating: GridItem(.flexible()), count: 3)
#State private var fullscreen = false
#State private var isPresented = false
var body: some View {
VStack{
ScrollView{
LazyVGrid(columns: gridLayout, spacing: 3) {
ForEach(greenappData.GreenAppGallery, id: \.self) { item in
Image(item)
.resizable()
.frame(width: UIScreen.main.bounds.width/3, height: 150)
.onTapGesture {
self.isPresented.toggle()
print(" tapping number")
}
.fullScreenCover(isPresented: $isPresented) {
FullScreenModalView( imageFiller: item)
}
.background(Color.gray.opacity(0.5))
}
}
.padding(.horizontal)
}
}
}
}
This is an example of the json data:
{
"id" : "1",
"GreenAppGallery" : [
"Picture-1",
"Picture-2",
"Picture-3",
"Picture-4",
"Picture-5",
"Picture-6",
"Picture-7",
"Picture-8",
"Picture-9",
"Picture-10"
]
},
fullScreenCover, like sheet tends to create this type of behavior in iOS 14 when using isPresented:.
To fix it, you can change to the fullScreenCover(item: ) form.
Not having all of your code, I'm not able to give you an exact version of what it'll look like, but the gist is this:
Remove your isPresented variable
Replace it with a presentedItem variable that will be an optional. Probably a datatype that is in your gallery. Note that it has to conform to Identifiable (meaning it has to have an id property).
Instead of toggling isPresented, set presentedItem to item
Use fullScreenCover(item: ) { presentedItem in FullScreenModalView( imageFiller: presentedItem) } and pass it your presentedItem variable
Move the fullScreenCover so that it's attached to the ForEach loop rather than the Image
Using this system, you should see it respond to the correct item.
Here's another one of my answers that covers this with sheet: #State var not updated as expected in LazyVGrid

Why does binding to the Picker not work anymore in swiftui?

When I run a Picker Code in the Simulator or the Canvas, the Picker goes always back to the first option with an animation or just freezes. This happens since last Thursday/Friday. So I checked some old simple code, where it worked before that and it doesn't work for me there, too.
This is the simple old Code. It doesn't work anymore in beta 3, 4 and 5.
struct PickerView : View {
#State var selectedOptionIndex = 0
var body: some View {
VStack {
Text("Option: \(selectedOptionIndex)")
Picker(selection: $selectedOptionIndex, label: Text("")) {
Text("Option 1")
Text("Option 2")
Text("Option 3")
}
}
}
}
In my newer code, I used #ObservedObject, but also here it doesn't work.
Also I don't get any errors and it builds and runs.
Thank you for any pointers.
----EDIT----- Please look at the answer first
After the help, that I could use the .tag() behind all Text()like Text("Option 1").tag(), it now takes the initial value and updates it inside the view. If I use #ObservedObject like here:
struct PickerView: View {
#ObservedObject var data: Model
let width: CGFloat
let height: CGFloat
var body: some View {
VStack(alignment: .leading) {
Picker(selection: $data.exercise, label: Text("select exercise")) {
ForEach(data.exercises, id: \.self) { exercise in
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
}
}
.frame(width: width, height: (height/2), alignment: .center)
}
}
}
}
Unfortunately it doesn't reflect changes on the value, if I make these changes in another view, one navigationlink further. And also it doesn't seem to work with the my code above, where I use firstIndex(of: exercise)
---EDIT---
Now the code above works if I change
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
into
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise)!)
because it couldn't work with an optional.
The answer summarized:
With the .tag() behind the Options it works. It would look like following:
Picker(selection: $selectedOptionIndex, label: Text("")) {
ForEach(1...3) { index in
Text("Option \(index)").tag(index)
}
}
If you use a range of Objects it could look like this:
Picker(selection: $data.exercises, label: Text("")) {
ForEach(0..<data.exercises.count) { index in
Text("\(data.exercises[index])").tag(index)
}
}
I am not sure if it is intended, that .tag() is needed to be used here, but it's at least a workaround.
I found a way to simplify the code a bit without the need of operating on indicies and tags.
At first, make sure to conform your model to Identifiable protocol like this (this is actually a key part, as it enables SwiftUI to differentiate elements):
public enum EditScheduleMode: String, CaseIterable, Identifiable {
case closeSchedule
case openSchedule
public var id: EditScheduleMode { self }
var localizedTitle: String { ... }
}
Then you can declare viewModel like this:
public class EditScheduleViewModel: ObservableObject {
#Published public var editScheduleMode = EditScheduleMode.closeSchedule
public let modes = EditScheduleMode.allCases
}
and UI:
struct ModeSelectionView: View {
private let elements: [EditScheduleMode]
#Binding private var selectedElement: EditScheduleMode
internal init?(elements: [EditScheduleMode],
selectedElement: Binding<EditScheduleMode>) {
self.elements = elements
_selectedElement = selectedElement
}
internal var body: some View {
VStack {
Picker("", selection: $selectedElement) {
ForEach(elements) { element in
Text(element.localizedTitle)
}
}
.pickerStyle(.segmented)
}
}
}
With all of those you can create a view like this:
ModeSelectionView(elements: viewModel.modes, selectedElement: $viewModel.editScheduleMode)