I have recently started coding for iOS and using Swift. I am trying to build a small quiz app for practice. However, I am having an issue running a function that is stored in an array.
My question library swift file is as follows:
func getQuestionLibrary() -> NSArray {
var questionLibrary = [
[
"categoryName": "General Knowledge",
"functionName": generalknowledgeLibrary()
]]
As you can see it states the category and stores a function.
My code that works fine, uses this array (there are more entries) to dynamically create a list of categories to choose from. When a category is run it performs a segue and moves onto a view to display the categories.
If I hard code in the categories that app works great:
if playQuestionLibraryText == "General Knowledge" {
questionPack = generalknowledgeLibrary()
} else if playQuestionLibraryText == "Music" {
questionPack = musicLibrary()
} else if playQuestionLibraryText == "Film" {
questionPack = filmLibrary()
}
However, as the list is dynamic I would prefer it not to be hard coded.
Please can you assist me to allow the my code to search the array and run the function stored as functionName in the array when the correct category has been selected.
Thank you in advance.
The code:
"functionName": generalknowledgeLibrary()
Sets "functionName" to the result of calling the function.
Use:
"functionName": generalknowledgeLibrary
You are looking up a 'library' based on its name; use a Dictionary. Your 'library' is going to hold some stuff (as libraries are wont to do) and allow some behaviors - so capture it as an abstraction.
class Library { // maybe this is a 'questionPack'
// stuff in a library // that is okay, change the name
}
var libraryMap : [String:Library] =
["Music": Library(/*...*/),
"Film" : Library(/*...*/),
"General Knowledge" : Library(/*...*/)
// ....
]
if let library = libraryMap[playQuestionLibraryText] {
// do something with the library
}
i did a code to this question, using subscript
I created a class collection to manage your question. I believe the friend solution just remove () is more easy and correctly, but i created this class to complement my studies, because this I like enter here to try new solutions.
class QuestionCollection
{
struct QuestionItem {
var categoryName:String;
var function:()->Void //Here can add return type you need
}
private var dicCallbacks:[String:QuestionItem] = [String:QuestionItem]();
func add(categoryName:String, closure:()->Void //Here can add return type you need )
{
dicCallbacks[categoryName] = QuestionItem(categoryName: categoryName, function: closure);
}
subscript(categoryName:String)->()->Void //Here can add return type you need
{
get
{
if let callback = self.dicCallbacks[categoryName]
{
return callback.function;
}
return error;
}
set
{
dicCallbacks[categoryName] = QuestionItem(categoryName: categoryName, function: newValue);
}
}
func error()->Void
{
println("error try catch closure function")
}
}
How use this class
func musicTest()
{
println("test Music");
}
func musicGK()
{
println("test Music");
}
func musicFilm()
{
println("test Music");
}
var questionCollection = QuestionCollection();
questionCollection["Music"] = musicTest
questionCollection["General Knowledge"] = musicGK
questionCollection["Film"] = musicFilm
questionCollection["Music"]();
questionCollection["General Knowledge"]();
questionCollection["Film"]();
Sorry for slow reply. I changed my code as this works instead and I am happy with how it works.
The questions in the future will be populated via the internet via a loop.
struct Question {
var categoryName : String
var questionTitle : String
var answerA : String
var answerB : String
var answerC : String
var answerD : String
var correct : String
}
public struct QuestionLibrary {
var questions: [Question]
}
let QuizQuestions =
QuestionLibrary(
questions: [
Question(
categoryName: "General Knowledge",
questionTitle: "Question 1",
answerA: "A", answerB: "B", answerC: "C", answerD: "D", correct: "D"),
Question(
categoryName: "Music",
questionTitle: "Question 2",
answerA: "A", answerB: "B", answerC: "C", answerD: "D", correct: "A"),
Question(
categoryName: "Film",
questionTitle: "Question 3",
answerA: "A", answerB: "B", answerC: "C", answerD: "D", correct: "B")
])
My code then to retrieve questions based on the category is:
let questionLibrary = QuizQuestions.questions
var questionPack: Array<Question>?
questionPack = questionLibrary.filter({c in c.categoryName == "Music" })
I then select a random question
let question = questionPack![randomNumber]
And to display the question text is
question.questionTitle
So, my earlier question was about running functions in an array which I learnt I didn't need to do. But least I've answered so it might have others who need similar code :)
Related
I'm a Windows C# developer, new to iOS/SwiftUI development and I think I've worked myself into a hole here.
I have a view with a #Binding variable:
struct DetailView: View {
#Binding var project: Project
The project is an object which contains an array of Tasks. I am looping through the tasks of the project to display its name and a toggle whose state is determined by the Task's variable, isComplete.
ForEach(filteredTasks.indices, id: \.self) { idx in
HStack {
Text(filteredTasks[idx].phase)
.font(.caption)
Spacer()
Text(filteredTasks[idx].name)
Spacer()
Toggle("", isOn: self.$filteredTasks[idx].isComplete)
}
}
}
This took quite a while for me to get to this piece of code, and I found that I had to follow an example with the 'indices' option to get the toggle to work on each Task individually, and to make sure that its isComplete value was saved.
Next, I wanted to filter the list of Tasks based on a Task variable, phase, which has values of Planning, Construction, or Final. So I created 4 buttons (one for each phase, and then an 'All Tasks' to get back to the full, unfiltered list), and after a lot of trial and error (creating filtered arrays that no longer were bound correctly, etc., etc.) I tried this, basically working only with the original array.
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
HStack {
Text(project.tasks[idx].phase)
.font(.caption)
Spacer()
Text(project.tasks[idx].name)
Spacer()
Toggle("", isOn: self.$project.tasks[idx].isComplete)
}
}
}
And of course, this seemed to work because I can do a test:
func CreateTestArray() {
let testFilterArray = project.tasks.filter({ $0.phase.contains(filterValue) })
}
And that will give me the filtered list I want. However, in my ForEach view, it's not working correctly and I'm not sure how to work around it.
For example, I have 128 tasks, 10 of which have a value of 'Final' and when I use a button setting the filterValue to Final, the testFilterArray actually contains the correct 10 tasks - but in the ForEach view I'm getting the first ten tasks in the original array (which are of the type 'Planning' - the original array is sorted by Planning/Construction/Final); obviously the ForEach, in spite of the filter statement, is working on the original array. The Planning button sends the filterValue = "Planning", and I get the correct results because the filter returns 0-19 indices for the 20 Planning tasks I have in the original array, and since they're first in the original array, it 'appears' that the Planning filter is working correctly, tho in actually it's just by chance that it works, if the array were sorted differently it would not.
Any ideas how I can approach this so that I can actually filter on this array, display the isComplete toggle correctly for each item in the array, as well as update the toggle state dynamically? I feel like I need to start from scratch once again here because I've let these constraints work me into a tiny Swift corner.
Thanks!
Update:
Thank you, #jnpdx, for your quick response - and I should definitely have included objects (which I list below). However, looking back over my object definitions, I wonder if I've made an even more basic error in managing the objects which is why I've gotten boxed in to the situation I have (i.e., in earlier iterations I'd attempted some of your suggestion). At any rate, my published object is 'projects', which is a list of projects I pass to the project list view, and then that view passes a single project to a project view, and then that view lists the tasks in that particular project.
I feel like your answer is pointing me in the right decision, I just need to back back up and look at those object definitions/management and see how to get to a situation where a straightforward solution is possible.
Task:
struct Task: Identifiable, Codable {
let id: UUID
var phase: String
var category: String
var name: String
var isComplete: Bool
init(id: UUID = UUID(), phase: String, category: String, name: String, isComplete: Bool) {
self.id = id
self.phase = phase
self.category = category
self.name = name
self.isComplete = isComplete
}
}
The Project:
struct Project: Identifiable, Codable {
var id: UUID
var name: String
var type: String
var tasks: [Task]
var isComplete: Bool
init(id: UUID = UUID(), name: String, type: String, tasks: [Task] = [], isComplete: Bool) {
self.id = id
self.name = name
self.type = type
self.tasks = tasks
self.isComplete = isComplete
}
}
and the Project model:
class ProjectData: ObservableObject {
// code to access the json file is here
// An accessible list of projects from the saved file
#Published var projects: [Project] = []
// load and save functions follow
Update:
Thanks, #jnpdx, your solution worked after making, as you said I would need to, the tweaks to get it to function within my particular model design. Here are the snippets that finally worked in my case.
In my view:
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) })) { task in
HStack {
Text(task.name)
Toggle("", isOn: self.makeBinding(item: task))
}
}
}
And the called function:
func makeBinding(item: Task) -> Binding<Bool> {
let i = self.project.tasks.firstIndex { $0.id == item.id }!
return .init(
get: { self.project.tasks[i].isComplete },
set: { self.project.tasks[i].isComplete = $0 }
)
}
Let's look at the following line from your code:
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
In the first part, you filter tasks and then ask for the indices. My suspicion is that you're hoping it would return something like [1, 5, 10, 11, 12], meaning their original positions in the array. But, in reality, you're going to get a contiguous array like [0,1,2,3,4] because it's giving you indices from the newly-created array (the result of filter).
There are a couple of ways to solve this, which also relate to the previous ForEach that you had.
It's more idiomatic to do ForEach and iterate over structs/objects rather than indices. You don't show what Task is made up of, but let's say it's this:
struct Task : Hashable {
var id = UUID()
var name: String
var phrase: String
var isComplete: Bool
}
To iterate on it, you could do:
ForEach(task, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id)) //explained later
}
I asked about the type of Project in my comment because I'm not totally clear why it's a #Binding. It seems like maybe it's an object? If it's a view model, which would be nice, you can handle your Toggle logic there. Something like:
class Project : ObservableObject {
#Published var tasks : [Task] = [Task(name: "1", phrase: "phase", isComplete: false),Task(name: "2", phrase: "phase", isComplete: true),Task(name: "3", phrase: "phase2", isComplete: false)]
var completedTasks : [Task] {
return tasks.filter { $0.isComplete }
}
func taskCompletedBinding(id: UUID) -> Binding<Bool> {
Binding<Bool>(get: {
self.tasks.first(where: { $0.id == id})?.isComplete ?? false
}, set: { newValue in
self.tasks = self.tasks.map { t in
if t.id == id {
var tCopy = t
tCopy.isComplete = newValue
return tCopy
} else {
return t
}
}
})
}
}
And you can test that it works doing this:
struct ContentView: View {
#ObservedObject var project = Project()
var body: some View {
ForEach(project.tasks, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id))
}
}
}
If Project is a struct and not an object, it might be good to wrap it in an ObservableObject view model like I did above.
I'm writing a basic truth or dare app as a practice project and I'm running into an issue when I try to make an array for truths/dares. I want to randomly call a dare but not have that dare be available as an option when I call the method again. I tried using array.remove(at: ) but I then run into issues with immutable classes/methods.
Any help would be appreciated.
var wildDares = [
"Wild Dare A",
"Wild Dare B",
"Wild Dare C",
"Wild Dare D",
"Wild Dare E"]
func randomWildDare() -> String {
let randomNum = GKRandomSource.sharedRandom().nextInt(upperBound: wildDares.count)
if wildDares.count == 1 {
return "You're out of dares. Select a new pack or click 'New Game' in the menu section." } else {
//wildDares.remove(at: randomNum)
return wildDares[randomNum]
}
}
I see you are using GameplayKit to shuffle your dares, but shuffle is built in to swift itself now.
struct DareStore {
private var dares: [String]
init(dares: [String]) {
self.dares = dares.shuffled()
}
mutating func next() -> String? {
if dares.count > 0 {
return dares.removeFirst()
}
return nil
}
}
In this DareStore you see we provide the init with an array of dares, and we there handle shuffling of it. Now whenever next() is called, it will just remove and return the first of the shuffled dares, until no more are left.
var dares = DareStore(dares: ["1", "2", "3"])
while let dare = dares.next() {
print(dare)
}
In Swift 4, how do you convert a nested for loop that is checking equality on only one property into a filter?
Basic Example:
// Basic object
struct Message {
let id: String
let content: String
init(id: String, content: String) {
self.id = id
self.content = content
}
}
// Array of objects
let local = [Message.init(id: "1234", content: "test1"), Message.init(id: "2345", content: "test2")]
// Array of objects, one has updated content
let server = [Message.init(id: "1234", content: "testDiff1"), Message.init(id: "3456", content: "test3")]
var foundList = [Message]()
// Nested loop to find based on one property matching
for i in local {
for j in server {
if i.id == j.id {
foundList.append(i)
}
}
}
This works as expected (foundList contains local[0]) but feeling there should be a 'swift-ier' way to do this?
for loops can be rewritten with one for loop + where condition:
for m in local where server.contains(where: { $0.id == m.id }) {
foundList.append(m)
}
or combine filter with contains:
foundList = local.filter { m in server.contains(where: { $0.id == m.id }) }
P.S. Also, conform Message struct to Equatable protocol. It allows you to simplify contains method:
for m in local where server.contains(m) {
foundList.append(m)
}
using filter:
foundList = local.filter { server.contains($0) }
Use filter.
import Foundation
struct Message: Equatable {
let id: String
let content: String
static func == (lhs: Message, rhs: Message) -> Bool {
return lhs.id == rhs.id
}
}
let local = [ Message(id: "1234", content: "test1"), Message(id: "2345", content: "test2") ]
let server = [ Message(id: "1234", content: "testDiff1"), Message(id: "3456", content: "test3") ]
let foundList = local.filter { server.contains($0) }
print(foundList) // prints [Message(id: "1234", content: "test1")]
Note that I removed the initialiser and I’m using Message(...) instead Message.init(...).
It should be easy to write a filter for this. I assume you want the local messages that are also on the server.
let m = local.filter
{
localMessage in
server.contains(where: { $0.id == localMessage.id })
}
If you have a lot of messages, it might be a good idea to create a set of interesting ids.
let filterIds = Set(server.map{ $0.id })
let m = local.filter { filterIds.contains($0) }
That will reduce the big O time complexity because you aren't doing a linear search through the set. The time complexity on contains(where:) for an array is going to be O(n) where n is the number of elements. For a set, Apple documents the complexity of contains() as O(1) Of course, there is an overhead for creating the set and for small n the linear search might be quicker than the set access.
Given I have array of stucts like this:
let array = [Struct(key: "a", value: 1), Struct(key: "b", value:2)]
How can I subscript the array with the key?
array["b"] would be nice, but as expected, it doesn't work.
Edit: The reason I'm not using dictionary, is I need to preserve the order of items.
This is syntactic sugar around your solution, #AdamBardon.
You can extend Array to allow you to subscript it directly. Under the covers it is just using the same first(where:) call:
protocol HasKey {
var key: String { get }
}
struct Struct: HasKey {
var key: String
var value: Int
}
extension Array where Element: HasKey {
subscript(str: String) -> Element? {
return self.first(where: { $0.key == str })
}
}
Example:
let array = [Struct(key: "a", value: 1), Struct(key: "b", value:2)]
if let x = array["a"] {
print(x)
}
Output:
Struct(key: "a", value: 1)
Using the protocol allows you to easily extend this functionality to any class or struct that has a key: String property by having them adopt the HasKey property:
extension SomeOtherClass: HasKey { }
You can also accomplish it without the protocol by checking if Element == Struct:
extension Array where Element == Struct {
subscript(str: String) -> Element? {
return self.first(where: { $0.key == str })
}
}
You can make the dictionary of dictionaries like below code:
let myDictionaryOfDictionaries : [String : [String : String]] =
["Apples" : ["Colour" : "Red", "Type" : "Granny Smith"],
"Oranges" : ["Colour" : "Orange", "Type" : "Seville"]]
print(myDictionaryOfDictionaries["Apples"] ?? "")
Hope it will help you.
You should use a dictionary. Arrays are designed to access elements by their index and not by a property of the element. Indexes also have to be integers.
Dictionaries on the other hand are key-value pairs, so using a Dictionary seems to perfect for your use case.
let structs = ["a":Struct(key:"a",value:1),"b":Struct(key:"b",value:2)]
structs["b"] returns the struct with key "b".
This is not the exact way how I imagined the solution, but it's a good alternative which solves my problem.
I got what I needed using this:
array.first(where: { $0.key == "b" })
I have some data returned from the server that look like this:
let returnedFromServer = ["title" : ["abc", "def", "ghi"],
"time" : ["1234", "5678", "0123"],
"content":["qwerty", "asdfg", "zxcvb"]]
I want to transform it into something like this:
let afterTransformation =
[["title" : "abc",
"time" : "1234",
"content": "qwerty"],
["title" : "def",
"time" : "5678",
"content": "asdfg"],
["title" : "ghi",
"time" : "0123",
"content": "zxcvb"]]
My current implementation is as follows:
var outputArray = [[String : AnyObject]]()
for i in 0..<(returnedFromServer["time"] as [String]).count {
var singleDict = [String: AnyObject]()
for attribute in returnedFromServer {
singleDict[attribute] = returnedFromServer[attribute]?[i]
}
outputArray.append(singleDict)
}
This works fine but I think it is not a very elegant solution. Given that Swift has some neat features such as reduce, filter and map, I wonder if I can do the same job without explicitly using a loop.
Thanks for any help!
Using the ideas and the dictionary extension
extension Dictionary {
init(_ pairs: [Element]) {
self.init()
for (k, v) in pairs {
self[k] = v
}
}
func map<OutKey: Hashable, OutValue>(transform: Element -> (OutKey, OutValue)) -> [OutKey: OutValue] {
return Dictionary<OutKey, OutValue>(Swift.map(self, transform))
}
}
from
What's the cleanest way of applying map() to a dictionary in Swift?,
you could achieve this with
let count = returnedFromServer["time"]!.count
let outputArray = (0 ..< count).map {
idx -> [String: AnyObject] in
return returnedFromServer.map {
(key, value) in
return (key, value[idx])
}
}
Martin R’s answer is a good one and you should use that and accept his answer :-), but as an alternative to think about:
In an ideal world the Swift standard library would have:
the ability to initialize a Dictionary from an array of 2-tuples
a Zip3 in addition to a Zip2 (i.e. take 3 sequences and join them into a sequence of 3-tuples
an implementation of zipWith (i.e. similar to Zip3 but instead of just combining them into pairs, run a function on the given tuples to combine them together).
If you had all that, you could write the following:
let pairs = map(returnedFromServer) { (key,value) in map(value) { (key, $0) } }
assert(pairs.count == 3)
let inverted = zipWith(pairs[0],pairs[1],pairs[2]) { [$0] + [$1] + [$2] }
let arrayOfDicts = inverted.map { Dictionary($0) }
This would have the benefit of being robust to ragged input – it would only generate those elements up to the shortest list in the input (unlike a solution that takes a count from one specific list of the input). The downside it its hard-coded to a size of 3 but that could be fixed with a more general version of zipWith that took a sequence of sequences (though if you really wanted your keys to be strings and values to be AnyObjects not strings you’d have to get fancier.
Those functions aren’t all that hard to write yourself – though clearly way too much effort to write for this one-off case they are useful in multiple situations. If you’re interested I’ve put a full implementation in this gist.
I'd create 2 helpers:
ZipArray (similar to Zip2, but works with arbitrary length):
struct ZipArray<S:SequenceType>:SequenceType {
let _sequences:[S]
init<SS:SequenceType where SS.Generator.Element == S>(_ base:SS) {
_sequences = Array(base)
}
func generate() -> ZipArrayGenerator<S.Generator> {
return ZipArrayGenerator(map(_sequences, { $0.generate()}))
}
}
struct ZipArrayGenerator<G:GeneratorType>:GeneratorType {
var generators:[G]
init(_ base:[G]) {
generators = base
}
mutating func next() -> [G.Element]? {
var row:[G.Element] = []
row.reserveCapacity(generators.count)
for i in 0 ..< generators.count {
if let e = generators[i].next() {
row.append(e)
}
else {
return nil
}
}
return row
}
}
Basically, ZipArray flip the axis of "Array of Array", like:
[
["abc", "def", "ghi"],
["1234", "5678", "0123"],
["qwerty", "asdfg", "zxcvb"]
]
to:
[
["abc", "1234", "qwerty"],
["def", "5678", "asdgf"],
["ghi", "0123", "zxcvb"]
]
Dictionary extension:
extension Dictionary {
init<S:SequenceType where S.Generator.Element == Element>(_ pairs:S) {
self.init()
var g = pairs.generate()
while let (k:Key, v:Value) = g.next() {
self[k] = v
}
}
}
Then you can:
let returnedFromServer = [
"title" : ["abc", "def", "ghi"],
"time" : ["1234", "5678", "0123"],
"content":["qwerty", "asdfg", "zxcvb"]
]
let outputArray = map(ZipArray(returnedFromServer.values)) {
Dictionary(Zip2(returnedFromServer.keys, $0))
}