How to run a function from switch statement in swiftui? - swiftui

I trying to run a function with parameter from a switch statement in swiftui but kept getting the "Type '()' cannot conform to 'View'" error. I think the switch statement and the function should be correct. No matter how I play around with the case statement, I'll still get the same error message.
struct questionsData: Codable {
enum CodingKeys: CodingKey {
case question
case answers
case correctAnswerIndex
}
//var id = UUID()
var question: String
var answers = [String]()
var correctAnswerIndex: Int
}
struct ThemeView: View {
var quizzes = [questionsData]()
let themeName: String
var body: some View {
let themeselected: String = themeName
var jsonfile: String = ""
switch themeselected {
case "Money Accepted":
jsonfile = "Accounts"
return loadQuizData(jsonname: jsonfile)
case "Computers":
jsonfile = "Computers"
return loadQuizData(jsonname: jsonfile)
default:
Text("invalid")
}
}
func loadQuizData(jsonname: String){
guard let url = Bundle.main.url(forResource: jsonname, withExtension: "json")
else {
print("Json file not found")
return
}
let data = try? Data(contentsOf: url)
var quizzes = try? JSONDecoder().decode([questionsData].self, from: data!)
quizzes = quizzes!
}
}
struct ContentView: View {
#State private var selection: String?
let quizList = ["Money Accepted","Computers","Making an appointment", "Late again", "Shopping", "Renting a place", "Accounts", "Letter Writing", "Planning a business", "Business Expression 1", "Business Expression 2", "How to ask the way"]
var body: some View {
NavigationView{
List(quizList, id:\.self) { quizList in
NavigationLink(destination: ThemeView(themeName: quizList)){
Text(quizList)
}
}
.navigationTitle("Select quiz theme")
}
}
}
Please kindly assist... still new to swiftui.
Greatly appreaciated.

I don't get exactly how your ThemeView should look like, maybe you can show us a preview. But there are some mistakes in there. Firstly, try to extract that logic in a ViewModel. Basically have a separate layer to handle business logic such as json parsing and other stuff. Secondly, try not to have var in views, unless they are marked as #State, #Binding, #ObservedObject... . Not least, SwiftUI views should create views not handle logic such as ViewControllers in UIKit, your switch create no view this is why it does not conform to View.

Related

in SwiftUI, I have 2 Entities (A & B) in my CoreData with a relationship (one to many) between them, how can I fetch all attributes of B in TextFields

Let's say I have 2 entities:
GameSession :which has Attributes "date", "place", "numberofplayer" + a relationship called "players" with "Player"
Player: which has Attributes "name","score_part1","score_part2","score_part3" + a relationship with "GameSession"
the relationship is "one to many": One session can have many players
Let's say now I have a list of GameSession and when I click on on one (with a NavigationLink)
It sends me to a new view where I can see:
All the names of the players of that session (in text) and also right next to the player name I would like to have 3 TextField in which I can enter (an update) "score_part1","score_part2","score_part3" for every players of that session
Basically I am able to display the name of all the players of a given session, But it seems impossible to have the "score_part1","score_part2","score_part3" in editable TextField...
I have an error saying "Cannot convert value of type 'String' to expected argument type 'Binding<String>'"
Basically in my first view I have something like that:
struct RamiListePartieUIView: View {#Environment(.managedObjectContext) var moc#FetchRequest(entity: GameSession.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \GameSession.date, ascending: false)]) var gamesessions: FetchedResults<GameSession>
var body: some View {
VStack {
List {
ForEach(gamesessions, id: \.date) { session in
NavigationLink (destination: DetailPartieSelecUIView(session: session)){
Text("\(session.wrappedPlace) - le \(session.wrappedDate, formatter: itemFormatter) ")
}
}
.onDelete(perform: deleteSessions)
.padding()
}
}
}
}
And in my second view I have something like that:
struct DetailPartieSelecUIView: View {
#State var session:GameSession
#Environment(\.managedObjectContext) var moc
var body: some View {
Section("Ma session du \(session.wrappedDate, formatter: itemFormatter)"){
ForEach(session.playersArray, id: \.self) { player in
HStack {
Text(player.wrappedName) // OK it works
TextField("score", text : player.wrappedScore_part1) // it generates an error
TextField("score", text : player.wrappedScore_part2) // it generates an error
TextField("score", text : player.wrappedScore_part3) // it generates an error
}
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .short
// formatter.timeStyle = .medium
formatter.dateFormat = "YYYY/MM/dd" //"YY/MM/dd"
return formatter
}()
also,
I have defined the "wrappedScore_part1","wrappedScore_part2","wrappedScore_part3" in the Player+CoreDataProperties.swift file
and "wrappedPlace", "wrappedData" as well as the "PlayersArray" in the GameSession+CoreDataProperties.swift file
it is done like that:
public var wrappedPlace: String {
place ?? "Unknown"
}
// Convert NSSet into an array of "Player" object
public var playersArray: [Player] {
let playersSet = players as? Set<Player> ?? []
return playersSet.sorted {
$0.wrappedName< $1.wrappedName
}
}
I am new at coding with swiftUI so I am probably doing something wrong... If anyone can help me it would be much appreciated.
Thanks a lot
I have tried a lot of things. Like changing the type of my attribute to Int32 instead os String. As I am suppose to enter numbers in those fields, I thought it would be best to have Integer. But it didn't change anything. and ultimately I had the same kind of error message
I tried also to add the $ symbol, like that:
TextField("score", text : player.$wrappedScore_part1)
But then I had other error message popping up at the row of my "ForEach", saying "Cannot convert value of type '[Player]' to expected argument type 'Binding'"
And also on the line just after the HStack, I had an error saying "Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'"
Thank you for your help!
Best regards,
JB
Your first problem of how to fetch the players in a session you need to supply a predicate to the #FetchRequest<Player>, e.g.
#FetchRequest
private var players: FetchedResults<Player>
init(session: Session) {
let predicate = NSPredicate(format: "session = %#", session)
let sortDescriptors = [SortDescriptor(\Player.timestamp)] // need something to sort by.
_players = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
}
That acts like a filter and will only return the players that have the session relation equalling that object. The reason you have to fetch like this is so any changes will be detected.
The second problem about the bindings can be solved like this:
struct PlayerView: View{
#ObservedObject var player: Player {
var body:some View {
if let score = Binding($player.score) {
TextField("Score", score)
}else{
Text("Player score missing")
}
}
}
This View takes the player object as an ObservedObject so body will be called when any of its properties change and allows you to get a binding to property. The Binding init takes an optional binding and returns a non-optional, allowing you to use it with a TextField.

SwiftUI parse Multilayer JSON Data from URL and build list

First an information: I am actually on the way learning swiftUI, I'm a total newbie. For my first project i decided to create a small app that loads articles from a joomla website. My API will respond to a query in the following structure:
{
"source": "my AppConnector for Joomla!",
"offset": 0,
"count": 0,
"results": [
{
"id": "8",
"title": "Article 1",
...
},
{
"id": "8",
"title": "Article 2",
...
}
]
}
In the future the API will return more complex structures but actually i'm struggling already with that one. All swiftUI examples & videos i've found are just explaining how to retreive an array of items or are to old and shows depreacet code examples (with the one-dimensional examples i have already successfully created a list view of items but that's not what i want).
I've created the following structs:
struct Welcome: Codable {
let source: String
let offset, count: Int
let results: [Result]
}
// MARK: - Result
struct Result: Codable {
let id, title, alias, introtext: String
let fulltext, state, catid, created: String
let createdBy, createdByAlias, modified, modifiedBy: String
let checkedOut, checkedOutTime, publishUp, publishDown: String
let images, urls, attribs, version: String
let ordering, metakey, metadesc, access: String
let hits, metadata, featured, language: String
let xreference, note, slug, articleID: String
let introImage, fullImage: String
}
and the following fetcher:
import Foundation
import SwiftUI
import Combine
public class ArticlesFetcher: ObservableObject {
#Published var articles = [Welcome]()
init(){
load()
}
func load() {
let url = URL(string: "https://nx-productions.ch/index.php/news")! //This is a public demo url feel free to check the jsondata (SecurityToken temporary disabled)
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([Welcome].self, from: d)
DispatchQueue.main.async {
self.articles = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
My view looks like this:
struct ContentView: View {
#ObservedObject var fetcher = ArticlesFetcher()
var body: some View {
VStack {
List(fetcher.articles.results) { article in
VStack (alignment: .leading) {
Text(article.title)
Text(article.articleId)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
What i don't understand is the view part - i am not able to point into the fields, with the example above i get compiler errors like "Value of type '[Welcome]' has no member 'results'" or "Value of type 'Int' has no member 'title'"
I think i may just not understand something aboutmy structure or how to loop through it.
Thanks for any advise.
The JSON starts with a { so it's a dictionary. And the type of articles is wrong.
Replace
#Published var articles = [Welcome]()
with
#Published var articles = [Result]()
and replace
let decodedLists = try JSONDecoder().decode([Welcome].self, from: d)
DispatchQueue.main.async {
self.articles = decodedLists
}
with
let decodedLists = try JSONDecoder().decode(Welcome.self, from: d)
DispatchQueue.main.async {
self.articles = decodedLists.results
}
Finally but not related replace meaningless
print ("Error")
with
print(error)

Passing Decoded JSON Data to SwiftUI ContentView

API call and JSON decoding are working fine, as I can print to console any item from the JSON data set without a problem.
Here's API call and test print:
import Foundation
import SwiftUI
import Combine
class APICall : ObservableObject {
#Published var summary: Summary?
init () {
pullSummary()
}
func pullSummary() {
let urlCall = URL(string: "https://api.covid19api.com/summary")
guard urlCall != nil else {
print("Error reaching API")
return
}
let session = URLSession.shared
let dataTask = session.dataTask(with: urlCall!) { (data, response, error) in
if error == nil && data != nil {
let decoder = JSONDecoder()
do {
let summary = try decoder.decode(Summary.self, from: data!)
print(summary.byCountry[40].cntry as Any)
DispatchQueue.main.async {
self.summary = summary
}
}
catch {
print("Server busy, try again in 5 min.")
}
}
}
dataTask.resume()
}
}
And here is the structure of the "Summary" data model used for the decoding and data object structure:
import Foundation
struct Summary: Decodable {
let global: Global
let byCountry: [ByCountry]
let date: String
enum CodingKeys: String, CodingKey {
case global = "Global"
case byCountry = "Countries"
case date = "Date"
}
struct Global: Decodable {
let globalNC: Int
let globalTC: Int
let globalND: Int
let globalTD: Int
let globalNR: Int
let globalTR: Int
enum CodingKeys: String, CodingKey {
case globalNC = "NewConfirmed"
case globalTC = "TotalConfirmed"
case globalND = "NewDeaths"
case globalTD = "TotalDeaths"
case globalNR = "NewRecovered"
case globalTR = "TotalRecovered"
}
}
struct ByCountry: Decodable {
let cntry: String?
let ccode: String
let slug: String
let cntryNC: Int
let cntryTC: Int
let cntryND: Int
let cntryTD: Int
let cntryNR: Int
let cntryTR: Int
let date: String
enum CodingKeys: String, CodingKey {
case cntry = "Country"
case ccode = "CountryCode"
case slug = "Slug"
case cntryNC = "NewConfirmed"
case cntryTC = "TotalConfirmed"
case cntryND = "NewDeaths"
case cntryTD = "TotalDeaths"
case cntryNR = "NewRecovered"
case cntryTR = "TotalRecovered"
case date = "Date"
}
}
}
As shown, the results of the API call and JSON decode are published as required using ObserveableObject and #Published.
Over in the ContentView, I have followed the ObservedObject rules and only want to display on the UI a data point from the JSON data to confirm it's working:
import SwiftUI
import Foundation
import Combine
struct ContentView: View {
#ObservedObject var summary = APICall()
var body: some View {
Text($summary.date)
.onAppear {
self.summary.pullSummary()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
BUT... I get these 2 errors at the Text display line, 1) Initializer 'init(_:)' requires that 'Binding<Subject>' conform to 'StringProtocol' and 2) Value of type 'ObservedObject<APICall>.Wrapper' has no dynamic member 'date' using the key path from root type 'APICall'.
I am guessing the 2nd error is the root cause of the problem, indicating the data is not being passed into the ContentView correctly.
I appreciate any suggestions.
Thanks.
It is messed view model with internal property
struct ContentView: View {
#ObservedObject var viewModel = APICall()
var body: some View {
Text(viewModel.summary?.date ?? "Loading...") // << no $ sign !!!
.onAppear {
self.viewModel.pullSummary()
}
}
}

Efficient way to model the data for SwiftUI

I am exploring SwiftUI+Combine with a demo app BP Management.
Homescreen has a provision to take bp readings(systolicBP, diastolicBP, pulse & weight).
Button "Next" is enabled only when all 4 fields are filled.
control should fall to the next textfield when a valid input is entered. (input is valid when it falls between the range specified by the placeholder - refer the image below)
On tapping next, on the detail screen user can edit the bp values (taken in the HomeScreen), additionally he can add recorded date, notes...
Thought enums would be best model this so I proceeded like
enum SBPInput: CaseIterable {
//name is a Text to indicate the specific row
typealias Field = (name: String, placeholder: String)
case spb, dbp, pulse, weight, note, date
var field: Field {
switch self {
case .dbp: return ("DBP", "40-250")
case .spb: return ("SBP", "50-300")
case .pulse: return ("Pulse", "40-400")
case .weight: return ("Weight", "30-350")
case .note: return ("Note", "")
case .date: return ("", Date().description)
}
}
// Here I am getting it wrong, - I can't bind a read only property
var value: CurrentValueSubject<String, Never> {
switch self {
case .date:
return CurrentValueSubject<String, Never>(Date().description)
case .spb:
return CurrentValueSubject<String, Never>("")
case .dbp:
return CurrentValueSubject<String, Never>("")
case .pulse:
return CurrentValueSubject<String, Never>("")
case .weight:
return CurrentValueSubject<String, Never>("70")
case .note:
return CurrentValueSubject<String, Never>("")
}
}
}
class HomeViewModel: ObservableObject {
#Published var aFieldsisEmpty: Bool = true
var cancellable: AnyCancellable?
var dataSoure = BPInput.allCases
init() {
var bpPublishers = (0...3).map{ BPInput.allCases[$0].value }
//If a field is empty, we need to disable "Next" button
cancellable = Publishers.CombineLatest4(bpPublishers[0], bpPublishers[1], bpPublishers[2], bpPublishers[3]).map { $0.isEmpty || $1.isEmpty || $2.isEmpty || $3.isEmpty }.assign(to: \.aFieldsisEmpty, on: self)
}
}
The idea is to create HStacks for each datasorce(sbp,dbp,pulse,weight) to look like this
struct HomeScreen: View {
#ObservedObject var viewModel = HomeViewModel()
var body: some View {
VStack {
ForEach(Range(0...3)) { index -> BPField in
BPField(input: self.$viewModel.dataSoure[index])
}
Button("Next", action: {
print("Take to the Detail screen")
}).disabled(self.viewModel.aFieldsisEmpty)
}.padding()
}
}
struct BPField: View {
#Binding var input: BPInput
var body: some View {
//implicit HStack
Text(input.field.name)
BPTextField(text: $input.value, placeHolder: input.field.name)//Error:- Cannot assign to property: 'value' is a get-only property
// input.value being read only I can't bind it. How to modify my model now so that I can bind it here?
}
}
And my custom TextField
struct BPTextField: View {
let keyboardType: UIKeyboardType = .numberPad
var style: some TextFieldStyle = RoundedBorderTextFieldStyle()
var text: Binding<String>
let placeHolder: String
// var onEdingChanged: (Bool) -> Void
// var onCommit: () -> ()
var background: some View = Color.white
var foregroundColor: Color = .black
var font: Font = .system(size: 14)
var body: some View {
TextField(placeHolder, text: text)
.background(background)
.foregroundColor(foregroundColor)
.textFieldStyle(style)
}
}
your problems are not there, what SwiftUI tells you.
but you should first compile "small parts" of your code and simplify it, so the compiler will tell you the real errors.
one is here:
BPTextField(text: self.$viewModel.dataSoure[index].value, placeHolder: viewModel.dataSoure[index].field.placeholder)
and the error is:
Cannot subscript a value of type 'Binding<[BPInput]>' with an argument of type 'WritableKeyPath<_, _>'
and of course you forgot the self ....

put observedObject in List

I get the data from my api and create a class for them. I can use swifyJSON to init them correctly. The problem is that when I put my observedObject in a List, it can only show correctly once. It will crashed after I changed the view. It's very strong because my other List with similar data struct can work.(this view is in a tabView) Is somebody know where my getAllNotification() should put view.onAppear() or List.onAppear()? Thanks!!
class ManagerNotification : Identifiable, ObservableObject{
#Published var id = UUID()
var notifyId : Int = 0
var requestId : Int = 0
var requestName: String = ""
var groupName : String = ""
// var imageName: String { return name }
init(jsonData:JSON) {
notifyId = jsonData["notifyId"].intValue
requestId = jsonData["requestId"].intValue
requestName = jsonData["requestName"].stringValue
groupName = jsonData["groupName"].stringValue
}
}
import SwiftUI
import SwiftyJSON
struct NotificationView: View {
var roles = ["userNotification", "managerNotification"]
#EnvironmentObject var userToken:UserToken
#State var show = false
#State private var selectedIndex = 0
#State var userNotifications : [UserNotification] = [UserNotification]()
#State var managerNotifications : [ManagerNotification] = [ManagerNotification]()
var body: some View {
VStack {
Picker(selection: $selectedIndex, label: Text(" ")) {
ForEach(0..<roles.count) { (index) in
Text(self.roles[index])
}
}
.pickerStyle(SegmentedPickerStyle())
containedView()
Spacer()
}
.onAppear(perform: getAllNotification)
}
func containedView() -> AnyView {
switch selectedIndex {
case 0:
return AnyView(
List(userNotifications) { userNotification in
UserNotificationCellView(userNotification: userNotification)
}
)
case 1:
return AnyView(
List(managerNotifications) { managernotification in
ManagerNotificationCellView(managerNotification : managernotification)
}
.onAppear(perform: getManagerNotification)
)
default:
return AnyView(Text("22").padding(40))
}
}
func getAllNotification(){
// if (self.userNotifications.count != 0){
// self.userNotifications.removeAll()
// }
// I think the crash was in here, because when i don't use removeAll().
// It works fine, but i don't want every times i change to this view. my array will be longer and
// longer
if (self.managerNotifications.count != 0){
self.managerNotifications.removeAll()
}
NetWorkController.sharedInstance.connectApiByPost(api: "/User/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.userNotifications.append(UserNotification(jsonData: notification))
}
}
}
}
}
NetWorkController.sharedInstance.connectApiByPost(api: "/Manager/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.managerNotifications.append(ManagerNotification(jsonData: notification))
}
}
}
}
}
}
func getManagerNotification(){
// if (self.managerNotifications.count != 0){
// self.managerNotifications.removeAll()
// }
print(self.managerNotifications.count)
NetWorkController.sharedInstance.connectApiByPost(api: "/Manager/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.managerNotifications.append(ManagerNotification(jsonData: notification))
}
}
}
}
}
}
}
error message
Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. reason: 'attempt to delete section 0, but there are only 0 sections before the update'
I think you are confused about the role of #State and #ObservebableObject; it's not like MVC where you replace the ViewController with a SwiftUI.View as it appears you are trying to do in your example. Instead the view should be a function of either some local #State and/or an external #ObservedObject. This is closer to MVVM where your #ObservedObject is analogous to the ViewModel and the view will rebuild itself in response to changes in the #Published properties on the ObservableObject.
TLDR: move your fetching logic to an ObservableObject and use #Published to allow the view to subscribe to the results. I have an example here: https://github.com/joshuajhomann/TVMaze-SwiftUI-Navigation