SwiftUI Random but unique elements - swift

I have a simple array of 3 fonts
var fonts = ["Arial", "System", "Helvetica"]
let randomFont: String
so the user is asked to guess one of the 3
VStack(spacing: 30) {
Text("Which font is \(randomFont)?")
so this correctly displays one of the 3 elements.
Each has to use a unique element of the fonts array and...if is the same font as per randomFont the counter should be increased by one.
Here is an hardcoded example:
Button(action: { self.score += 1 }) {
Text("\(randomWord)").font(.custom("Helvetica", size: 40))
}
Button(action: { self.score += 0 }) {
Text("\(randomWord)").font(.custom("Arial", size: 40))
}
Button(action: { self.score += 0 }) {
Text("\(randomWord)").font(.system(size: 40))
}
I need help to randomise the fonts but all 3 have to be used. Additionally I need to compare the random font selected by the user with the font displayed in the question. if is the same (right answer) a point is added.
so for instance
randomFont is "Arial"
the buttons will display 3 words in a random font (e.g.randomFont2 variable) (Arial, Helvetica or system all have to be used and no duplicates) if the user clicks the button with the Arial font +1 point is added to the counter.

Here's a complete standalone example. I used 3 very distinct fonts (Courier, System, and Papyrus) for testing purposes.
fonts.shuffle() is used to randomize the fonts.
The three buttons only differ in the font index they are using and looking for, so I've used a private function to contruct the buttons to avoid repetition.
The randomly chosen randomFontIndex uses Int.random(in: 0..<3) to choose a number that is 0, 1, or 2. To add more fonts, just add more font names to the fonts array. Make sure to select valid font names though!
import SwiftUI
struct ContentView: View {
let words = ["birds", "meadow", "butterfly", "flowers"]
#State private var randomFontIndex = 0
#State private var randomFont = "Courier"
#State private var fonts = ["Courier", "System", "Papyrus"]
#State private var score = 0
#State private var randomWord = ""
var body: some View {
VStack(spacing: 30) {
Text("Score: \(score)")
Text("Which font is \(randomFont)?")
buttonFromNumber(0)
buttonFromNumber(1)
buttonFromNumber(2)
}.onAppear { self.newGame() }
}
private func fontFromName(name: String) -> Font {
switch name {
case "System":
return Font.system(size: 40)
default:
return Font.custom(name, size: 40)
}
}
private func buttonFromNumber(_ number: Int) -> some View {
Button(action: {
if number == self.randomFontIndex {
self.score += 1
} else {
self.score -= 1
}
self.newGame()
}) {
Text("\(randomWord)").font(fontFromName(name: fonts[number]))
}
}
private func newGame() {
fonts.shuffle()
randomFontIndex = .random(in: 0..<3)
randomFont = fonts[randomFontIndex]
randomWord = words.randomElement()!
}
}

Related

SwiftUI - adding an extra row in a list every 4 rows

In my code I display numbers in a list. User can choose the grouping method and the numbers will be put into sections accordingly (either groups of 5 elements or odd/even). Now, I would like to add a green row after 4*n elements where n=1,2,3,.. as seen from the UI perspective (not the perspective of the data source !). So after the fourth row, a green row should follow. After eighth row, a green row should follow etc.
In my current code this works for the groups of 5 elements but does not work for the odd/even variant. The problem seems to be in the indexes because they don't depend on the actual placement.
I know this seems to sound a bit complicated but maybe someone has an idea how to approach the problem, ideally in some scalable way so that if I add a third grouping method in the future it will all also work.
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var myViewModel = MyViewModel()
var body: some View {
VStack {
Button {
myViewModel.groupStrategy.send(myViewModel.groupStrategy.value == .multiplesOfFive ? .oddEven : .multiplesOfFive)
} label: {
Text("Toggle grouping strategy")
}
List() {
ForEach(myViewModel.numOfSections, id:\.self) { sectNum in
Section("Sc \(sectNum)") {
ForEach(Array(myViewModel.nums.enumerated()), id: \.offset) { idx, element in
let _ = print("Sc \(sectNum) \(idx) \(element)")
if myViewModel.shouldSkipNumberInThisSection(number: element, sectionNumber: sectNum) {
EmptyView()
} else {
Text(element.description + " idx: " + idx.description)
if idx > 0 && (idx+1) % 4 == 0 {
Color.green
}
}
}
}
}
}
}
.padding()
}
}
class MyViewModel: ObservableObject {
enum GroupStrategy {
case multiplesOfFive
case oddEven
}
#Published var nums: [Int]
#Published var numOfSections: [Int] = []
var groupStrategy = CurrentValueSubject<GroupStrategy, Never>(.multiplesOfFive)
private var cancellables: Set<AnyCancellable> = []
func shouldSkipNumberInThisSection(number: Int, sectionNumber: Int) -> Bool {
switch groupStrategy.value {
case .multiplesOfFive:
return number >= sectionNumber * 5 || number < (sectionNumber-1) * 5
case .oddEven:
return sectionNumber == 0 ? (number % 2) == 0 : (number % 2) != 0
}
}
func shouldPutGreenRow() -> Bool {
return false
}
init() {
self.nums = []
let numbers: [Int] = Array(3...27)
self.nums = numbers
self.numOfSections = Array(1..<Int(nums.count / 5)+1)
groupStrategy.sink { strategy in
switch self.groupStrategy.value {
case .multiplesOfFive:
self.numOfSections = Array(1..<Int(self.nums.count / 5)+1)
case .oddEven:
self.numOfSections = Array(0..<2)
}
}.store(in: &cancellables)
}
}
For the multiplesOfFive group - OK:
For the odd/even group - NOT OK:
In the odd/even group, the green row should appear after numbers 9, 17, 25, 8, 16, 24. Instead, it appears only in the group of even numbers
The code is not working for the odd/even section because, for each section, you are iterating over all numbers and re-creating the indexes (or idx in your code).
So, the condition if idx > 0 && (idx+1) % 4 == 0 is good for the multiples of five, instead for odd/even it should be if idx > 0 && (idx + sectNum + 2) % 8 == 0. However, if you just add this condition to the view, it gets less scalable, as you will need to create new conditions for each different grouping.
I have thought of an alternative to your View Model using a dictionary within a dictionary, to store the sections, the indexes and the values, with no repeated entries.
With this approach, the view model gets a little bit more complicated, but the underlying data is the same, and the view is scalable: if you create a new GroupingStrategy, you don't need to change the view.
Here's the code with comments, I hope it might help you:
A bold view model:
class MyViewModel: ObservableObject {
// Create raw values and conform to CaseIterable,
// so you don't need to change the view when creating new strategies
enum GroupStrategy: String, CaseIterable {
case multiplesOfFive = "Groups of 5 items"
case oddEven = "Odd vs. even"
}
// This is the underlying data: no changes
#Published var nums = Array(3...27)
// This is where the sections are stored: a dictionary that
// includes the numbers within and their position in the final sequence.
#Published var sequence = [Int:[Int:Int]]()
// It can work with any type of underlying data, not just Int.
// For example, if the data is of type MyModel, this variable can be of type
// [Int:[Int:MyModel]]
#Published var groupStrategy = GroupStrategy.multiplesOfFive {
didSet {
// Will rebuild the dictionary every time the grouping changes
rebuildGroupStrategy()
}
}
// Extract all the sections for the current strategy
var sections: [Int] {
Array(Set(sequence.keys)).sorted()
}
// Extract all the numbers for a specific section: the key of
// the dictionary is the index in the sequence
func numsInSection(_ section: Int) -> [Int:Int] {
return sequence[section] ?? [:]
}
// Define the strategies in the Enum, then here...
var arrayOfSections: [Int] {
switch groupStrategy {
case .multiplesOfFive:
return Array(1..<Int(nums.count / 5) + 1)
case .oddEven:
return [0, 1]
}
}
// ... and here
func isIncludedInSection(number: Int, section: Int) -> Bool {
switch groupStrategy {
case .multiplesOfFive:
return number < section * 5 && number >= (section - 1) * 5
case .oddEven:
return section == 0 ? (number % 2) != 0 : (number % 2) == 0
}
}
// When you need to set a new strategy
func rebuildGroupStrategy() {
sequence = [:]
var prog = 0
// Create the sequence, which will not contain repeated elements
arrayOfSections.forEach { section in
sequence[section] = [:]
nums.forEach { number in
if isIncludedInSection(number: number, section: section) {
sequence[section]?[prog] = number
prog += 1
}
}
}
}
init() {
rebuildGroupStrategy()
}
}
A simpler view:
struct MyView: View {
#StateObject var myViewModel = MyViewModel()
var body: some View {
VStack {
// Use a Picker for scalability
Picker("Group by:", selection: $myViewModel.groupStrategy) {
ForEach(MyViewModel.GroupStrategy.allCases, id: \.self) { item in
Text(item.rawValue)
}
}
List() {
// Iterate over the sections
ForEach(myViewModel.sections, id:\.self) { sectNum in
Section("Sc \(sectNum)") {
let numbers = myViewModel.numsInSection(sectNum)
// Iterate only over the numbers of each section
ForEach(numbers.keys.sorted(), id: \.self) { index in
Text("\(numbers[index] ?? 0), Index = \(index)")
// Same condition ever: paint it green after every 4th row
if (index + 1) % 4 == 0 {
Color.green
}
}
}
}
}
}
.padding()
}
}

Using DidSet on array of Strings to have character limit doesn't work

I am trying to create a character limit for a TextField which corresponds to a String within an array with the following code but it doesn't seem to work. The following object is a poll object where the user can add TextFields in which they can write in the option they desire. I'm trying to build a max character limit for each string in which if a TextField exceed the value of the max character limit, the array of Strings (or the violating string) is replaced with the old value. Right now, the print statements indicate that the values are being updated and fixed at the max character limit, but on the View, I can still type beyond the max character limit which is odd since the value in the TextField should be bound to the published variable.
VStack {
ForEach(0..<newPollVM.pollOptions.count, id: \.self) { i in
HStack {
TextField("Option \(i + 1)", text: $newPollVM.pollOptions[i])
.font(.title3)
.padding(5)
.overlay(RoundedRectangle(cornerRadius: 5).stroke(Color(UIColor.systemGray), lineWidth: 2))
Spacer()
if newPollVM.pollOptions.count > 2 {
Button {
print("DEBUG: Remove row")
newPollVM.pollOptions.remove(at: i)
} label: {
Image(systemName: "delete.left")
.foregroundColor(.red)
}
}
}
.padding(.top, 5)
}
if newPollVM.pollOptions.count < 6 {
Button {
print("DEBUG: Add Option")
newPollVM.pollOptions.append("")
print("DEBUG: \(newPollVM.pollOptions)")
} label: {
Text("Add")
.foregroundColor(.green)
}
}
class NewPollViewModel: ObservableObject {
let characterLimit = 5
#Published var allowSkipVoting : Bool = false
#Published var pollOptions : [String] = [""] {
didSet {
for (index, pollOption) in pollOptions.enumerated() {
if pollOption.count > characterLimit && oldValue[index].count <= characterLimit {
print("DEBUG: \(oldValue)")
self.pollOptions[index] = oldValue[index]
print("DEBUG: \(pollOptions)")
}
}
}
}
func reset() {
self.allowSkipVoting = false
self.pollOptions = ["", ""]
}
}
However, the following code works regarding setting a character limit for a single String.
TextArea("What's on your mind?", text: $newPostVM.title)
.font(.title)
class NewPostViewModel: ObservableObject {
let characterLimit = 180
#Published var title : String = "" {
didSet {
if title.count > characterLimit && oldValue.count <= characterLimit {
title = oldValue
}
}
}
}
This isn't what you asked for exactly. But I think it solves your problem in a better way. You can just use the limit field on your UITextField. Something like...
yourTextField.textLimit(existingText: textField.text,
newText: string,
limit: 180)
Here is a helpful link that shows a good way to do it.

Function in Swift

I am trying to learn swift by building out a very basic barbell weight plate calculator - the user enters a number in the text field and it displays the plates needed.
I wrote a function in Swift Playgrounds that works well for I am trying to do, but I don't understand how to move it into an app view where the user enters a number and it filters.
I have tried looking online for an explanation to this without any luck: Here is my Swift Playground code which is ideally what I would like to use:
import UIKit
func barbellweight (weight: Int){
var plate_hash : [Int:Int] = [:]
if weight == 45 {
print("You only need the bar!")
}else if weight < 45{
print("Must be divisible by 5!")
}else if (weight % 5 != 0){
print("Must be divisible by 5!")
}else{
let plate_array = [45, 35, 25, 10, 5, 2.5]
var one_side_weight = Double(weight - 45) / 2.0
for plate_size in plate_array {
var plate_amount = (one_side_weight / plate_size)
plate_amount.round(.towardZero)
one_side_weight -= (plate_size * plate_amount)
plate_hash[Int(plate_size)] = Int(plate_amount)
}
}
let plate_hash_filtered = plate_hash.filter { $0.value > 0 }
//print(plate_hash_filtered)
print(plate_hash_filtered)
}
barbellweight(weight: 225)
Here is attempt to implement it in Swift UI but without any luck. I know it's deconstructed and slightly different - I don't quite understand how to integrate a function into SwiftUI. If someone has any recommendations for resources to look at for this specific ask I would really appreciate it.
import SwiftUI
struct Weight_Plate: View {
#State var weight: String = "135"
#State var plate_hash = [String]()
#State var plate_array = [45, 35, 25, 10, 5, 2.5]
var body: some View {
var one_side_weight = Double(Int(weight)! - 45) / 2.0
List{
Text("Number of Plates Needed Per Side")
.multilineTextAlignment(.center)
ForEach(self.plate_array, id: \.self) { plate_size in
var plate_amount = (one_side_weight / plate_size)
if Int(weight) == 45 {
Text("You only need the bar!")
} else if Int(weight)! < 45 {
Text("Must be divisible by 5!")
} else if (Int(weight)! % 5 != 0) {
Text("Must be divisible by 5!")
} else {
//Text("Error")
plate_amount.round(.towardZero)
one_side_weight -= (plate_size * plate_amount)
Text("\(Int(plate_size)) x \(Int(plate_amount))")
// Text("\(plate):\(Int(plate_amount))")
}
}
HStack(alignment: .center) {
Text("Weight:")
.font(.callout)
.bold()
TextField("Enter Desired Weight", text: $weight)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
}
}
}
struct Weight_Plate_Previews: PreviewProvider {
static var previews: some View {
Weight_Plate()
}
}
I appreciate any help and recommendations on references that would assist me with this. Thank you!
You know you can still define and call functions right?
Change your code to this
import SwiftUI
func barbellWeight (weight: Int) -> [String] { // naming convention in Swift is camelcase
var plate_hash = [Int: Int]()
if weight == 45 {
return ["You only need the bar!"]
} else if weight < 45 {
return ["Insufficient weight!"]
} else if weight % 5 != 0 {
return ["Must be divisible by 5!"]
} else {
let plate_array = [45, 35, 25, 10, 5, 2.5]
var one_side_weight = Double(weight - 45) / 2.0
for plate_size in plate_array {
var plate_amount = (one_side_weight / plate_size)
plate_amount.round(.towardZero)
one_side_weight -= plate_size * plate_amount
plate_hash[Int(plate_size)] = Int(plate_amount)
}
}
return plate_hash.compactMap {
if $0.value < 0 {
return nil
} else {
return "\($0.key): \($0.value)"
}
}
}
struct Weight_Plate: View {
#State var weight = "135"
var body: some View {
List {
ForEach(barbellWeight(weight: Int(weight) ?? 135), id: \.self) {
Text($0)
}
}
HStack(alignment: .center) {
Text("Weight:")
.font(.callout)
.bold()
TextField("Enter Desired Weight", text: $weight)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
}
}
struct Weight_Plate_Previews: PreviewProvider {
static var previews: some View {
Weight_Plate()
}
}
Of course you can play around with this and make it look better. There are also other ways of doing this that could be more efficient, but this works.
Edit: You just define functions the same way you normally would! I rewrote your function to return an array of strings, each string corresponding to a plate, count pair as you can see when you run it. Since weight is a #State variable, when you change it by accepting your user's input, SwiftUI automatically reloads any view dependent on that variable. This causes the ForEach to be reloaded and your function to be called again. ForEach accepts an array of items, so that's why I glued the key and value of the dictionary together. Maybe this clears it up a little bit.
For help learning SwiftUI, check out Paul Hudson's site. It's how I started learning SwiftUI and how I transitioned to UIKit.
https://www.hackingwithswift.com/100/swiftui

SwiftUI's ForEach crashes when array's element is removed

I try to lay out my UI elements in rows - two elements in a row, for that I use two ForEach, one for rows and one for elements in a row. Each UI element has #Binding so I pass a structure from my model's array. I should be able to add or remove the elements dynamically and everything works except one thing - when I remove the only element from a row my app crashes with error Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444. I have read several topics on SO but I haven't found an answer.
This is how my code looks like:
struct PickerElement: Hashable {
let id: Int
let title: String
let value: Int
}
struct CellModel: Hashable {
var element: PickerElement?
var error: String?
}
struct PickerButton: View {
#Binding var value: CellModel?
#Binding var error: String?
(...)
}
class MyModel: ObservableObject {
#Published var counter = 0
#Published var cellModels = [CellModel]()
private var cancellables = Set<AnyCancellable>()
$counter.sink { value in
let diff = value - self.cellModels.count
if diff > 0 {
self.cellModels.append(contentsOf:
Array(repeating: CellModel(), count: diff)
)
} else if diff < 0 {
self.cellModels = Array(
self.cellModels.prefix(value)
)
}
}.store(in: &cancellables)
}
struct MyView: View {
#ObservedObject var model: MyModel
var body: some View {
VStack(spacing: 8) {
layOutElements()
}
}
#ViewBuilder
func layOutElements() -> some View {
let elementsCount = model.cellModels.count
if elementsCount > 0 {
VStack {
HStack {
Spacer()
Text("Some title").font(.caption)
Spacer()
}.padding()
// count number of rows
let rowsCount = Int(ceil(Double(elementsCount) / 2.0))
// lay out rows
ForEach(0 ..< rowsCount, id: \.self) { rowIndex in
layOutRow(rowIndex: rowIndex,
elementsCount: elementsCount,
rowsCount: rowsCount)
}
}
}
}
#ViewBuilder
private func layOutRow(rowIndex: Int, elementsCount: Int, rowsCount: Int) -> some View {
HStack(alignment: .top, spacing: 8) {
let firstCellInRowIndex = rowIndex * 2
let lastCellInRowIndex = min(elementsCount - 1, firstCellInRowIndex + 1)
ForEach(firstCellInRowIndex ... lastCellInRowIndex, id: \.self) { elementIndex in
PickerButton(value: $model.cellModels[elementIndex].element, // <--- *1
error: $model.cellModels[elementIndex].error) // <--- *2
}
// *1 , *2 - if I changed the lines and pass dummy bindings (not array's elements) there, the code would work without any glitches
if rowIndex == rowsCount - 1 && !elementsCount.isMultiple(of: 2) {
Spacer()
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
If I change a value of #Published var counter = 0 everything works properly, views are added and removed, but while decrementing if SwiftUI tries to remove the last remaining element from a row the app crashes. As I have commented in the code above, if I don't bind PickerButton to the structures from my model's array, the app doesn't crash. How to fix this issue? (I need to use indexes because I have to count rows and cells in a row)
The crash happens when elementsCount in layOutElements() becomes 0. Then layOutElements() doesn't return a View, which should be impossible. You can add an else to your if statement that returns EmptyView() or some placeholder.
Other than that, in my experience ForEach with dynamic ranges is a mess. If you need indexes you can use Array.enumerated() which has given me better results.

SwiftUI example for autocompletion

I'm a SwiftUI beginner. I have an array of "values" provided by an API and what I want is to make autocompletion when we tap characters in a "textfield". Can you please provide me an example of code for SwiftUI which can do this stuff ?
What I mean by autocompletion is this :
I have my own values and not those provided by google such here;
thx
The code from this repository used: https://github.com/simonloewe/TextFieldInputPrediction
And modified so the predictions are returned as a list like this:
//
// ContentView.swift
// StackOverflow
//
// Created by Simon Löwe on 04.04.20.
// Copyright © 2020 Simon Löwe. All rights reserved.
//
import SwiftUI
struct ContentView: View {
#State var textFieldInput: String = ""
#State var predictableValues: Array<String> = ["First", "Second", "Third", "Fourth"]
#State var predictedValue: Array<String> = []
var body: some View {
VStack(alignment: .leading){
Text("Predictable Values: ").bold()
HStack{
ForEach(self.predictableValues, id: \.self){ value in
Text(value)
}
}
PredictingTextField(predictableValues: self.$predictableValues, predictedValues: self.$predictedValue, textFieldInput: self.$textFieldInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
// This is the only modification from the example in the repository
List() {
ForEach(self.predictedValue, id: \.self){ value in
Text(value)
}
}
}.padding()
}
}
/// TextField capable of making predictions based on provided predictable values
struct PredictingTextField: View {
/// All possible predictable values. Can be only one.
#Binding var predictableValues: Array<String>
/// This returns the values that are being predicted based on the predictable values
#Binding var predictedValues: Array<String>
/// Current input of the user in the TextField. This is Binded as perhaps there is the urge to alter this during live time. E.g. when a predicted value was selected and the input should be cleared
#Binding var textFieldInput: String
/// The time interval between predictions based on current input. Default is 0.1 second. I would not recommend setting this to low as it can be CPU heavy.
#State var predictionInterval: Double?
/// Placeholder in empty TextField
#State var textFieldTitle: String?
#State private var isBeingEdited: Bool = false
init(predictableValues: Binding<Array<String>>, predictedValues: Binding<Array<String>>, textFieldInput: Binding<String>, textFieldTitle: String? = "", predictionInterval: Double? = 0.1){
self._predictableValues = predictableValues
self._predictedValues = predictedValues
self._textFieldInput = textFieldInput
self.textFieldTitle = textFieldTitle
self.predictionInterval = predictionInterval
}
var body: some View {
TextField(self.textFieldTitle ?? "", text: self.$textFieldInput, onEditingChanged: { editing in self.realTimePrediction(status: editing)}, onCommit: { self.makePrediction()})
}
/// Schedules prediction based on interval and only a if input is being made
private func realTimePrediction(status: Bool) {
self.isBeingEdited = status
if status == true {
Timer.scheduledTimer(withTimeInterval: self.predictionInterval ?? 1, repeats: true) { timer in
self.makePrediction()
if self.isBeingEdited == false {
timer.invalidate()
}
}
}
}
/// Capitalizes the first letter of a String
private func capitalizeFirstLetter(smallString: String) -> String {
return smallString.prefix(1).capitalized + smallString.dropFirst()
}
/// Makes prediciton based on current input
private func makePrediction() {
self.predictedValues = []
if !self.textFieldInput.isEmpty{
for value in self.predictableValues {
if self.textFieldInput.split(separator: " ").count > 1 {
self.makeMultiPrediction(value: value)
}else {
if value.contains(self.textFieldInput) || value.contains(self.capitalizeFirstLetter(smallString: self.textFieldInput)){
if !self.predictedValues.contains(String(value)) {
self.predictedValues.append(String(value))
}
}
}
}
}
}
/// Makes predictions if the input String is splittable
private func makeMultiPrediction(value: String) {
for subString in self.textFieldInput.split(separator: " ") {
if value.contains(String(subString)) || value.contains(self.capitalizeFirstLetter(smallString: String(subString))){
if !self.predictedValues.contains(value) {
self.predictedValues.append(value)
}
}
}
}
}
Provides the following outcome:
Tested on Version 11.5 and iOS 13.5