I'm currently following a series of lectures from Stanford to learn Swift (Code is from lectures 1-4 plus the homework I'm trying to complete) and am having an issue with setting an instance var(non-static) from a static function. I have gotten this to work but I would like it to be able to have different instances of the game have different themes
class EmojiMemoryGame: ObservableObject {
//Create the model in the viewController
#Published private var cardGame = EmojiMemoryGame.createMemoryGame()
static let themesArray = [
theme(name: "Halloween", color: Color.orange, emojiArray: ["💀","🕷","👻","🎃","👽","🍬","🌑"])
{6},
theme(name: "Aqua", color: Color.blue, emojiArray: ["🏴☠️","🐬","💧","⚓️","🏝","🐚","⛵️","🦑","🐡","🐠"]) {Int.random(in: 5...7)},
theme(name: "Animals", color: Color.gray, emojiArray: ["🦜","🐈","🐓","🦝","🐇","🐖","🐎","🦌"])
{Int.random(in: 3...4)}
]
static var selectedTheme: theme?
//Public Static Function
//Input: None
//Output: MemoryGame<String> struct
//Usage: Return the card game with emoijs inited
static func createMemoryGame() -> MemoryGame<String> {
//ISSUE
//Want to get a random theme from the themes array and assign it to an instance of this class but get an error beceause the function is static
selectedTheme = themesArray[Int.random(in: 0..<themesArray.count)]
let emojisArray = selectedTheme!.emojiArray
//Create the actual MemoryGame
return MemoryGame<String>(numberOfPairsOfCards: selectedTheme!.numberOfCards(), cardContentFactory: { cardPairIndex in
emojisArray[cardPairIndex]
})
}
//Struct Theme
struct theme {
let name: String
let color: Color
let emojiArray: [String]
let numberOfCards: () -> Int
}
I want to be able to get a random theme from themesArray but let selectedTheme be an instance var. Can anyone give me some advice?
You don't need to declare your selectedTheme as static.
You just need to add an initialiser and change what is returned from the function that creates your memory game. I've removed some of the code to make it easier to see what I have changed.
Notice that we have removed the setting of cardGame to the init. We have also removed all reference to selectedTheme from createMemoryGame. The createMemoryGame function now returns a tuple, which means that we can easily access the game and theme that it has created.
class EmojiMemoryGame: ObservableObject {
#Published private var cardGame: MemoryGame<String>
var selectedTheme: Theme // structs and classes should have capital letters
/// This now returns a tuple of the game we have created and the theme that we have chosen
static func createMemoryGame() -> (MemoryGame<String>, Theme) {
let theme = EmojiMemoryGame.themesArray[Int.random(in: 0..<themesArray.count)]
let emojisArray = theme!.emojiArray
let game = MemoryGame<String>(numberOfPairsOfCards: theme!.numberOfCards(), cardContentFactory: { cardPairIndex in
emojisArray[cardPairIndex]
})
return (game, theme)
}
// We add an initialiser to our class so that all the values that we need are initialised
init() {
let (game, theme) = EmojiMemoryGame.createMemoryGame() // note that the use of the names game and theme are arbitrary.
self.cardGame = game
self.selectedTheme = theme
}
}
Just let your ThemesArray be a static variable like it is. However, you won't need to declare selectedTheme as static. As you say, it is an instance variable.
Declare selectedTheme as following:
var selectedTheme: theme?
Then initialize it like the following:
selectedTheme = EmojiMemoryGame.themesArray[Int.random(in: 0..<EmojiMemoryGame.themesArray.count)]
let emojisArray = selectedTheme!.emojiArray
Related
I want to use an observable object in another class.
I want to use pitchers
import Foundation
import SwiftUI
class PositionViewModel: ObservableObject, Identifiable {
#Published var Pitchers: [String] = ["--","--","--","--","--","--"]
}
in this class then be able to pass it to different functions to validate rules.
class ValidationLogic: ObservableObject {
#Published var Positions: PositionViewModel = PositionViewModel()
var TempArray: [String];.self//error here
init(){
TempArray = Positions.Pitchers
}
static func Validation(Position: String, FrameStatus: Bool){
confirmNoBackToBack(Position: Position, FrameStatus: FrameStatus)
}
static func confirmNoBackToBack(Position: String, FrameStatus: Bool){
}
}
I have gotten to this point and am now getting an Expected Declaration error on the bold line. Not sure if I am just this last error from getting this to work or doing this the completely wrong way. I can use pitchers in a view but cant see to get it passed to my validationlogic class.
This is not valid Swift syntax, replace that line with:
var TempArray: [String]
Then modify you init to initialize Positions there:
var Positions: PositionViewModel
var TempArray: [String]
init() {
self.Positions = PositionViewModel()
TempArray = Positions.Pitchers
}
I am trying to save a small amount of data with picker using AppStorage across multiple views. However the issue I'm running into is that when I select one value and link to AppStorage it changes the value for all the others. What I want is to save the value for each selection over multiple views. If I use #State variable the selections work fine, but the values don't get saved when I close and reopen the app. I'm pretty sure I need to send each selection to it's own #AppStorage variable, but I'm having a hard time figuring out how to do that.
struct Animals: Identifiable {
var id = UUID().uuidString
var name: String
var animalTypes: [AnimalTypes]
}
var allAnimals = [
Animals(name: "fred", animalTypes: [.shiba, .lab]),
Animals(name: "barney", animalTypes: [.shiba, .dobberman, .lab]),
Animals(name: "molly", animalTypes: [.snowshoe, .burmese, .bengal]),
Animals(name: "bob", animalTypes: [.burmese]),
Animals(name: "wilma", animalTypes: [.snowshoe, .bengal]),
]
enum AnimalTypes: String, CaseIterable, Codable {
// Dog Breeds
case shiba, lab, dobberman
// Cat Breeds
case snowshoe, burmese, bengal
}
struct AnimalsView: View {
#State var animals: Animals!
var body: some View {
TabView {
ForEach(allAnimals) { animal in
AnimalSelectionView(animals: animal)
.tag(animal.name)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
struct AnimalSelectionView: View {
#AppStorage("selection") var animalSelection: Int = 0 // Saves the same value across all pickers (how to save individual values?)
// #State private var animalSelection: Int = 0 // Sets value for each picker in each tabview, but doesn't save
#State var animals: Animals!
var body: some View {
VStack {
if animals.animalTypes.count <= 1 {
Text("\(animals.animalTypes[0].rawValue)")
} else {
Text("\(animals.animalTypes[animalSelection].rawValue)")
}
if animals.animalTypes.count > 1 {
Picker(selection: $animalSelection, label: Text("Select Animal Type")) {
ForEach(0 ..< animals.animalTypes.count, id:\.self) { item in
Text("\(item + 1)")
.font(.caption)
}
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: 100, height: 17)
}
}
}
}
I see that you have decided to create a property on your Animals class called id. Happily, this is the perfect thing to use to save unique UserDefaults values for each Animals object.
You can do something like this:
struct AnimalSelectionView: View {
#AppStorage var animalSelection: Int
#State var animals: Animals
init(animals: Animals) {
self.animals = animals
// The below line simply assigns the key which should be used to access your
// desired variable. This way, it can be keyed specifically to your `Animals`
// id value.
self._animalSelection = AppStorage(wrappedValue: 0, "selection-\(animals.id)")
}
...
}
Keep in mind that you won't be able to retrieve the same value if you instantiate two different objects that have the same arguments passed in the init.
Example:
let fred = Animals(name: "fred", animalTypes: [.shiba, .lab])
let fred2 = Animals(name: "fred", animalTypes: [.shiba, .lab])
Edit for clarity:
Here, fred and fred2 are mostly the same, but the id property for each of these instances of Animals will be different. Therefore, if you try to access the selection value of one using the other's id, you will not receive the correct value. You must hold on to the exact Animals instance to access the value that you stored in UserDefaults, or at least hold on to its id as needed. This rule holds true across app sessions. If you instantiate an Animals object and then quit and reboot the app, when that same Animals object is re-instantiated, it will be assigned a different id value as per your definition Animals.id = UUID().uuidString.
In order to persist their ids across app sessions, you can either store the animals in CoreData, UserDefaults, grant them a static id, or generate an id based on the Animals properties.
Creating a static id would look like this:
`Animals(id: "animal-fred", name: "fred", animalTypes: [.shiba, .lab])`
Generating an id based on their properties might look like:
Animals: Identifiable {
var id: {
// We concatenate the name with each of the `AnimalsTypes`
// by mapping the `AnimalsTypes` to their relevant strings
// and joining them by a comma.
// The resulting `id` might look like:
// "fred-shiba,lab"
return name + "-" + animalTypes.map { $0.rawValue }.joined(separator: ",")
}
var name: String
var animalTypes: [AnimalTypes]
}
The method of generating ids based on the Animals properties works well, although if you create two Animals with the exact same properties, your logic won't be able to tell them apart as they will generate the same id. They will then fetch and set the same value in UserDefaults because they would share the same key.
If you intend for these animals to be created dynamically by the users of your app, you will need to store them either in CoreData or UserDefaults. If, however, your animals will all be created by you, you can either statically define the ID or generate them based on the Animals properties.
I have a HighlightedText View which is initialized with a string, a default font and a highlight font. The string can have markers to indicate which portions of it need to be highlighted. That all works fine. Now, instead of initialize it with a default font and a highlight font, I would like to be able to write this in a more swift-ui way to allow for more flexibility and improve readiness. So I would like to be able to do something like this:
HighlightedText("My text <highlighted>")
.defaultFont(.body)
.highlightFont(.title)
I know the standard way should be using a ViewModifier, however all I get from within its body function is a Content type and there doesn't seem to be a way I could cast it into my HighlightedText view and configure it as needed. All I seem to be able to do from within its body is just call other modifiers from View protocol, but that's not enough for my use case.
I've tried this extension, where defaultFont is a file private #State property defined in HighlightedText:
extension HighlightedText {
func defaultFont(_ font: Font) -> some View {
defaultFont.font = font
return body
}
}
That however, does not work. The default font I pass over never gets applied.
Here is possible solution:
if to you have declaration like
struct HighlightedText: View {
var defaultFount = Font.body
var highlightedFont = Font.headline
// ... other code
then your custom modifiers could be as
extension HighlightedText {
func defaultFont(_ font: Font) -> Self {
var view = self
view.defaultFount = font
return view
}
func highlightedFont(_ font: Font) -> Self {
var view = self
view.highlightedFont = font
return view
}
}
I am building a custom View struct with a large and complicated model that I want to update from within the var body : some View { ... } property (ex. tapping on a button representing a table column in the view should change the sort order of the rows in the table). I am not allowed to modify this model from within body because self is immutable however:
struct MyTable : View {
struct Configuration {
// a LOT of data here, ie header, rows, footer, style, etc
mutating func update() { ... } // this method must be called before view render
}
var configuration : Configuration
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // error, `self` is immutable
self.configuration.update() // error, `self` is immutable
}
}
I really don't want to create #State variables for all of the configuration data because 1) there's a lot of configuration data, 2) it would be difficult to model the model in such a way.
I tried instead making configuration a #State var, but I am unable to set the configuration object at init() time even though the code compiles and runs! (BTW, the configuration var now needs to be initialized before init, otherwise I get an error on the line self.configuration = c that self is used before all members are initialized -- this is likely a complication with using #State which is a property wrapper.)
struct MyTable : View {
struct Configuration {
...
}
#State var configuration : Configuration = Configuration() // must initialize the #State var
init(configuration c: Configuration) {
self.configuration = c // does not assign `c` to `configuration` !!!
self.$configuration.wrappedValue = c // this also does not assign !!!
self.configuration.update()
}
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // ok, no error now about mutating a #State var
self.configuration.update() // ok, no error
}
}
I came up with a work around where I don't need to call update() in MyTable.init() by creating a custom init() in Configuration that will call update(). This way the init() in MyTable is unnecessary and this approach resolves all previously encountered problems:
struct MyTable : View {
struct Configuration {
...
init(...) {
...
self.update()
}
mutating func update() { ... } // this method must be called before view render
}
#State var configuration : Configuration // note that this property can't be marked private because we want to inject `configuration` at init
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // ok, no error now about mutating a #State var
self.configuration.update() // ok, no error
...
}
}
Then in my calling code:
return MyTable.init(configuration: MyTable.Configuration.init(...))
Is it possible to create a global #State variable in SwiftUI that can be accessed across multiple Swift UI files?
I've looked into #EnvironmentObject variables but can't seem to make them do what I want them to do.
As of Beta 3 you cannot create a top-level global #State variable. The compiler will segfault. You can place one in a struct and create an instance of the struct in order to build. However, if you actually instantiate that you'll get a runtime error like: Accessing State<Bool> outside View.body.
Probably what you're looking for is an easy way to create a binding to properties on a BindableObject. There's a good example of that in this gist.
It is possible to create a Binding to a global variable, but unfortunately this still won't do what you want. The value will update, but your views will not refresh (code example below).
Example of creating a Binding programmatically:
var globalBool: Bool = false {
didSet {
// This will get called
NSLog("Did Set" + globalBool.description)
}
}
struct GlobalUser : View {
#Binding var bool: Bool
var body: some View {
VStack {
Text("State: \(self.bool.description)") // This will never update
Button("Toggle") { self.bool.toggle() }
}
}
}
...
static var previews: some View {
GlobalUser(bool: Binding<Bool>(getValue: { globalBool }, setValue: { globalBool = $0 }))
}
...