How to stop swiftui from moving content off screen in HStack? - swift

I want the SF Images to automatically go to the next line. I understand that HStack would keep content on the same line but, I am not able to come up with a solution on preventing it from going off screen. I have also tried using geometry reader but that also doesn't work.
https://i.stack.imgur.com/nLUzb.png
struct SplitTextView: View {
static let input = "B, LB, Y, RT, A, X, B, Right, X, LB, LB, LB"
let letters = input.components(separatedBy: ", ")
var body: some View {
GeometryReader { geo in
HStack (spacing: 10) {
ForEach(0..<self.letters.count) { index in
ButtonGeneratorView(buttonKey: self.letters[index])
}
}.frame(width: geo.size.width/2)
}
}
}
struct ButtonGeneratorView: View {
let buttonKey: String
let color: [String : Color] = ["Y" : Color.yellow,
"B" : Color.red,
"A" : Color.green,
"X" : Color.blue
]
var body: some View {
VStack {
if buttonKey.count == 1 {
Image(systemName: "\(buttonKey.lowercased()).circle.fill")
.font(.system(size: 32))
.foregroundColor(color["\(buttonKey)"])
}
else {
Image(systemName: "questionmark.circle.fill")//SF images for unknown
.font(.system(size: 32))
}
}
}
}

SwiftUI 2.0
Use LazyVGrid as in demo below
struct SplitTextView: View {
static let input = "B, LB, Y, RT, A, X, B, Right, X, LB, LB, LB"
let letters = input.components(separatedBy: ", ")
let layout = [
GridItem(.adaptive(minimum:32), spacing: 10)
]
var body: some View {
LazyVGrid(columns: layout, spacing: 10){
ForEach(0..<self.letters.count) { index in
ButtonGeneratorView(buttonKey: self.letters[index])
}
}
}
}
SwiftUI 1.0
There is no native built-in feature for this. The solution for similar problem I provided in SwiftUI HStack with wrap and dynamic height, and it can be adapted for this task as well.

Related

How to scale individual image within a forEach loop?

I have a ListView that has 20 items and uses the following Image within the ForEach loop:
Image(systemName: "heart").foregroundColor(.red).onTapGesture {
selected.toggle()
favLists.append(country.id)
favLists = favLists.removingDuplicates()
}
.scaleEffect(self.selected ? 1.5 : 1)
The issue is that selected is a single variable, so that is toggling the state for ALL my items in the list. How can I declare dynamic state dependent on the number of index items?
try this approach, ...to scale individual image within a ForEach loop, works for me:
struct ContentView: View {
#State var selected = "heart"
var body: some View {
VStack (spacing: 55) {
ForEach(["globe", "house", "person", "heart"], id: \.self) { name in
Image(systemName: name).id(name)
.onTapGesture {
selected = name
// ....other code
}
.foregroundColor(selected == name ? .red : .blue)
.scaleEffect(selected == name ? 2.5 : 1)
}
}
}
}

SwiftUI: How can I update an array of [struct]'s variable with a #Binding?

To preface, I'm nearly one month into learning SwiftUI and I've been watching a few YouTube channels to learn (e.g. Swiftful Thinking (Paul Hudson), Hacking with Swift (Nick Sarno), Sean Allen, etc.).
I'm testing something in Playgrounds and I need to implement a #Binding variable and, as such, I've watched all three of the aforementioned channel's tutorials on #Binding. The tutorials are explained clearly and I understand the concept, but I still seem to be running into a wall
I have a struct which contains a View — named TactileTrigger — that creates a draggable Circle(). I want to send the coordinates of this drag to another struct which contains a View — named CreateNewTrigger — that actually creates new instances of the TactileTrigger struct based on an array of information.
From within the CreateNewTrigger struct, I receive the error message: Cannot use instance member 'bindingXY' within property initializer; property initializers run before 'self' is available.
I've searched StackOverflow and have seen this same error, and I did try to implement an init() within the struct but, apparently, I'm still doing something wrong. Therefore, I have removed the init().
To clarify, I need the [TrigInformation]'s XY value updated for each respective $binding. I made a mockup in SwiftUI as an example of what I'm after:
CreateNewTactileTrigger:
import SwiftUI
class NotesManager: ObservableObject {
#Published var manager: [TrigInformation] = [
TrigInformation(trigNumber: 1,
trigType: .note,
noteValue: .Db,
XY: //<-- NEEDS TO APPEND HERE
),
TrigInformation(trigNumber: 2,
trigType: .note,
noteValue: .C,
XY: //<-- NEEDS TO APPEND HERE
),
TrigInformation(trigNumber: 3,
trigType: .note,
noteValue: .Eb,
XY: //<-- NEEDS TO APPEND HERE
),
TrigInformation(trigNumber: 4,
trigType: .trigger,
XY: //<-- NEEDS TO APPEND HERE
)
]
}
struct CreateNewTactileTrigger: View {
#StateObject var notesManager = NotesManager()
var body: some View {
VStack {
ForEach($notesManager.manager) { $note in
TactileTrigger(label: "\(note.trigNumber.description): [\(note.noteValue?.rawValue ?? "T")]",
bindingXY: $note.XY)
.frame(width: 25, height: 25)
.onAppear {
// notesManager.manager.append(
// TrigInformation(trigNumber: note.trigNumber,
// trigType: note.trigType,. <-- SOMETHING LIKE THIS
// noteValue: note.noteValue,
// XY: note.XY)
// )
}
VStack {
Text("\(note.trigNumber)")
Text("\(note.trigType.rawValue)")
Text("\(note.noteValue?.rawValue ?? "—")")
Text("X: \(note.XY.x)")
Text("Y: \(note.XY.y)")
}
.font(.caption)
.foregroundColor(.white)
.offset(x: 25,
y: 25)
}
}
}
}
struct TrigInformation: Identifiable {
let id = UUID()
var trigNumber: Int
var trigType: TrigType
var noteValue: Notes?
var XY: CGPoint
}
enum TrigType: String {
case trigger
case note
}
enum Notes: String {
case Ab = "Ab"
case A = "A"
case Bb = "Bb"
case B = "B"
case C = "C"
case Db = "Db"
case D = "D"
case Eb = "Eb"
case E = "E"
case F = "F"
case Gb = "Gb"
case G = "G"
}
Tactile Trigger:
import SwiftUI
struct TactileTrigger: View {
#State var label: String = ""
#State var setLocation: CGPoint = CGPoint(x: 100,
y: 100)
#Binding var bindingXY: CGPoint
var body: some View {
ZStack {
Circle()
.fill(.blue)
.overlay(
Text("\(label)").bold()
.font(.subheadline)
.foregroundColor(.white)
)
.frame(width: 75,
height: 75)
.position(x: setLocation.x,
y: setLocation.y)
.gesture(
DragGesture()
.onChanged({ currentPosition in
calculateDrag(value: currentPosition)
})
.onEnded({ endPosition in
calculateDrag(value: endPosition)
})
)
}
}
func calculateDrag(value: DragGesture.Value) {
let coordinates = CGPoint(x: value.location.x,
y: value.location.y)
setLocation = CGPoint(x: coordinates.x,
y: coordinates.y)
// BINDING VARIABLE
bindingXY = setLocation
}
}
MyApp:
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
CreateNewTactileTrigger()
}
}
}
At first every state must be initialized (you have uninitialised so error), at second you try to bind all notes to one state (even if you'd fix first error, you would got wrong behavior). You need own binding for each note in ForEach.
But better to bind directly to model.
So,
struct CreateNewTactileTrigger: View {
// #State var bindingXY: CGPoint // << remove !!
NoteInformation(trigNumber: 1,
trigType: .note,
noteValue: .Db,
XY: .zero // << initialize !!
),
ForEach($notes) { $note in // << work with bindings
TactileTrigger(label: "\(note.trigNumber.description): [\(note.noteValue?.rawValue ?? "T")]",
bindingXY: $note.XY) // << bind directly
no more changes, tested with Xcode 13.4 / iOS 15.5
I've rekindled my deleted answer and updated it to cater for your new question about an array of [TrigInformation].
Try this approach (works well for me), where you have a class NotesManager: ObservableObject, that holds your array of [TrigInformation]. As the trigArray element changes, the ObservableObject will be notified
and the UI refreshed with the new XY positions.
// -- here
class NotesManager: ObservableObject {
#Published var trigArray: [TrigInformation] = [
TrigInformation(trigNumber: 1,trigType: .note,noteValue: .Db, XY: CGPoint(x: 123, y: 123)),
TrigInformation(trigNumber: 2,trigType: .note,noteValue: .C,XY: CGPoint(x: 456, y: 456)),
TrigInformation(trigNumber: 3,trigType: .note,noteValue: .Eb,XY: CGPoint(x: 789, y: 789)),
TrigInformation(trigNumber: 4,trigType: .trigger, XY: CGPoint(x: 101, y: 101))
]
}
struct CreateNewTactileTrigger: View {
#StateObject var notesManager = NotesManager() // <-- here
var body: some View {
VStack {
ForEach($notesManager.trigArray) { $note in // <-- here
TactileTrigger(label: "\(note.trigNumber.description): [\(note.noteValue?.rawValue ?? "T")]",
bindingXY: $note.XY)
.frame(width: 25, height: 25)
VStack {
Text("\(note.trigNumber)")
Text("\(note.trigType.rawValue)")
Text("\(note.noteValue?.rawValue ?? "—")")
Text("X: \(note.XY.x)")
Text("Y: \(note.XY.y)")
}
.font(.caption)
.foregroundColor(.red) // <-- here for testing
.offset(x: 25, y: 25)
}
}
}
}

Multiple controls on same line in SwiftUI macOS form

I am making a UI to change the 3D coordinates of an object, and I thought it would make sense to put all three on the same line with a label beforehand, sort of like System Preferences does for number separators :
However, doing so messes up the alignment of the whole form, and I'm not sure how to resolve this (except by adding VStacks and HStacks everywhere, which I really hope is not the best available solution) :
Here is the code driving the view :
struct ObjectSettingsView: View {
#State var object : Object
var body : some View {
Form {
TextField("Name:", text: $object.name, prompt : Text("New Object"))
Toggle(
"Visible",
isOn: $object.visible
)
Divider()
HStack {
Text("Coordinates:")
NumberView(label : "X:", number : object.coordinates.x)
NumberView(label : "Y:", number : object.coordinates.y)
NumberView(label : "Z:", number : object.coordinates.z)
}
}
}
}
struct NumberView : View{
var label : String
#State var number : Int32
var body : some View {
HStack {
TextField(
self.label,
value: self.$number,
formatter: NumberFormatter()
)
Stepper("", value: self.$number, in: 1...8)
.labelsHidden()
}
}
}
( I know this really should be using a ViewModel, I'm just trying to figure out how forms work right now )
I add #Binding and LazyVGrid to your Code.
Maybe this helps:
struct ObjectData {
var name: String = "New Object"
var visible: Bool = true
var coordinates_x: Int32 = 0
var coordinates_y: Int32 = 0
var coordinates_z: Int32 = 0
}
struct ContentView: View {
#State var data = ObjectData()
let columns = [
GridItem(alignment: .trailing),
GridItem(alignment: .leading),
]
var form: some View {
LazyVGrid(columns: columns) {
Text("Name:")
TextField("", text: $data.name)
Text("blind").opacity(0)
Toggle("Visible:", isOn: $data.visible)
Text("Coordinates:")
HStack {
NumberView(label : "X:", number : $data.coordinates_x)
NumberView(label : "Y:", number : $data.coordinates_y)
NumberView(label : "Z:", number : $data.coordinates_z)
}
}
}
var body : some View {
VStack() {
form
Text(" --- Check --- ")
Text(String(describing: data))
}
.frame(width: 400.0)
}
}
struct NumberView : View{
var label : String
#Binding var number : Int32
var body : some View {
HStack {
TextField(
self.label,
value: self.$number,
formatter: NumberFormatter()
)
Stepper("", value: self.$number, in: 1...8)
.labelsHidden()
}
}
}
Separating things into two Forms almost does the trick, although labels are still not exactly aligned as in system Preferences :
struct ObjectSettingsView: View {
#State var object : Object
var body : some View {
VStack {
Form {
TextField("Name:", text: $object.name, prompt : Text("New Object"))
Toggle("Visible", isOn: $object.visible)
}
Divider()
Form {
HStack {
Text("Coordinates:")
NumberView(label : "X:", number : object.coordinates.x)
NumberView(label : "Y:", number : object.coordinates.y)
NumberView(label : "Z:", number : object.coordinates.z)
}
}
}
.padding()
}
}
// static let left = GridItem.Size.flexible(minimum: 10, maximum: .infinity)
static let left = GridItem.Size.flexible(minimum: 40, maximum: 100)
static let right = GridItem.Size.flexible(minimum: 40, maximum: 200)
let columns = [
GridItem(left, alignment: .trailing),
GridItem(right, alignment: .leading),
]
The problem is that the size structs are set to .infinity
I changed the column settings like above with fixed max values.
To get all infos see Apple Docs about GridItem.

SwiftUI The compiler is unable to type-check this expression in reasonable time

I could compile and run these code successfully before I updated Xcode to version 13.0. But this error shows up now.
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
I tried to break down my views and reproduce it like this. I found the problem is on line 21.
The code:
import SwiftUI
struct EmojiView: View {
var EmojiArr: [Int] = [0x1f601, 0x1f602, 0x1f603, 0x1f604, 0x1f605, 0x1f606, 0x1f607, 0x1f608, 0x1f609]
func GetVstackNum(num: Int) -> Int {
if num % 3 == 0 {
return num / 3
} else {
return num / 3 + 1
}
}
var body: some View {
HStack(spacing: 10) {
let length = EmojiArr.count
if length > 0 {
ForEach(0 ... (GetVstackNum(num: length) - 1), id: \.self) { i in
VStack(alignment: .leading, spacing: 22) {
ForEach((3 * i) ... (3 * i + 2), id: \.self) { j in
if j < length {
Button(action: {
// Some button action
}) {
if (UnicodeScalar(EmojiArr[j])?.properties.isEmoji)! {
Text(String(UnicodeScalar(EmojiArr[j])!)).font(.system(size:17))
}
else {
Text("")
}
}
.frame(width: 20, height: 10, alignment:.topLeading)
.buttonStyle(PlainButtonStyle())
}
}
}
.frame(height: 83, alignment:.topLeading)
}
}
}
}
}
How to fix this?
Your Code worked fine when I tested it using xcode 12.3 on macOS 11.2.3 . You can try using return HStack and see if the problem still persists. The above answer has some good tips as well
import SwiftUI
struct EmojiView: View {
var EmojiArr: [Int] = [0x1f601, 0x1f602, 0x1f603, 0x1f604, 0x1f605, 0x1f606, 0x1f607, 0x1f608, 0x1f609]
func GetVstackNum(num: Int) -> Int {
if num % 3 == 0 {
return num / 3
} else {
return num / 3 + 1
}
}
var body: some View {
return HStack(spacing: 10) {
let length = EmojiArr.count
if length > 0 {
ForEach(0 ... (GetVstackNum(num: length) - 1), id: \.self) { i in
VStack(alignment: .leading, spacing: 22) {
ForEach((3 * i) ... (3 * i + 2), id: \.self) { j in
if j < length {
Button(action: {
// Some button action
}) {
if (UnicodeScalar(EmojiArr[j])?.properties.isEmoji)! {
Text(String(UnicodeScalar(EmojiArr[j])!)).font(.system(size:17))
}
else {
Text("")
}
}
.frame(width: 20, height: 10, alignment:.topLeading)
.buttonStyle(PlainButtonStyle())
}
}
}
.frame(height: 83, alignment:.topLeading)
}
}
}
}
}
Generally, with SwiftUI it's better to try to minimize the number of conditionals that are directly within a View's body. (harder to grok, can cause extra runtime re-rendering, and as here, cause Xcode to run out of patience (possibly dependent on vintage of the build mac) trying to figure out what's going on 🙂). The Xcode problem occurs particularly if the branches of the conditionals result in the return of a different View type.
The solution, as hinted by Xcode, is to abstract out parts so that Xcode can check each bit more easily and match return View types.
With Xcode 13 beta 4 and iOS15 the following works for me and doesn't attempt to melt my intel mac:
import PlaygroundSupport
import SwiftUI
import SwiftUI
struct EmojiView: View {
let emojiArr: [Int] = [0x1F601, 0x1F602, 0x1F603, 0x1F604, 0x1F605, 0x1F606, 0x1F607, 0x1F608, 0x1F609]
var length: Int { emojiArr.count }
func getVstackNum(num: Int) -> Int {
if num % 3 == 0 {
return num / 3
} else {
return num / 3 + 1
}
}
func customButton(_ j: Int) -> some View {
return Button(action: {
// Some button action
}) {
if (UnicodeScalar(emojiArr[j])?.properties.isEmoji)! {
Text(String(UnicodeScalar(emojiArr[j])!)).font(.system(size: 17))
} else {
Text("")
}
}
}
func customRows(_ i: Int) -> some View {
return VStack(alignment: .leading, spacing: 22) {
ForEach((3 * i) ... (3 * i + 2), id: \.self) { j in
if j < length {
customButton(j)
.frame(width: 20, height: 10, alignment: .topLeading)
.buttonStyle(PlainButtonStyle())
}
}
}
}
var body: some View {
HStack(spacing: 10) {
if length > 0 {
ForEach(0 ... (getVstackNum(num: length) - 1), id: \.self) { i in
customRows(i)
.frame(height: 83, alignment: .topLeading)
}
}
}
.padding()
}
}
let view = EmojiView()
PlaygroundPage.current.setLiveView(view)
You should do what the error message tells you.
I think the struggle the compiler has, is that the if j < length { ... } does not have
an else case with a View. Try this as a test (but do what the error message tells you), works for me on macos 12, xcode 13-beta5 (not release):
var body: some View {
HStack(spacing: 10) {
let length = EmojiArr.count
if length > 0 {
ForEach(0 ... (GetVstackNum(num: length) - 1), id: \.self) { i in
VStack(alignment: .leading, spacing: 22) {
ForEach((3 * i) ... (3 * i + 2), id: \.self) { j in
if j < length {
Button(action: { }) {
if let emo = UnicodeScalar(EmojiArr[j]), emo.properties.isEmoji {
Text(String(emo)).font(.system(size:17))
} else {
Text("")
}
}
.frame(width: 20, height: 10, alignment:.topLeading)
.buttonStyle(PlainButtonStyle())
} else {
Text("") // <-- here
}
}
}
.frame(height: 83, alignment:.topLeading)
}
}
}
}
And don't use force unwrap in your code.

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